堆栈攻击基础

时间:2024-03-06 21:12:06

Structure

可以用来劫持控制流的关键点用黑体加粗

地址从低到高
.Text Gadgets
.Got Function Pointers
.Bss File Pointers
New Stack Data
Cannary
Saved Registers
Return Address
Old Stack
Old Chunk
New Chunk Prev Size(P=0)/Prev Data(P=1)
Size(高地址)&AMP(低地址)
Data Fd
Bk
Top Chunk
Libc Hook Pointers
One Gadgets
Vsyscall Gadgets

标志位

A:NON_MAIN_ARENA,记录当前 chunk 是否不属于主线程,1表示不属于,0表示属于。

M:IS_MAPPED,记录当前 chunk 是否是由 mmap 分配的。

P:PREV_INUSE,记录前一个 chunk 块是否被分配。

Top chunk

当所有的bin都无法满足用户请求的大小时,分割top chunk产生新chunk

初始情况下,我们可以将 unsorted chunk 作为 top chunk。

main_arena

与 thread 不同的是,main_arena 并不在申请的 heap 中,而是一个全局变量,在 libc.so 的数据段。

Startup

Linux环境下程序的加载过程

Linux x86 Program Start Up

Command

ls -ali 查看inode

exec 1>&2 恢复重定向

Shell

命令

onegadget,system,execve

跳板

malloc_hook,free_hook,got,rop,return addr,(虚表,堆喷)

Libc

泄露:unsorted bin UAF(超大chunk调用mmap),读取got(无leak需要先构造puts/printf),任意读->DynELF,构造printf读取栈上的__libc_start_main返回地址

Printf

可控格式化参数,利用%n%p进行任意读/写

Stack

栈中shellcode

ret gadget->jmp rsp跳板

bss中shellcode

控制ret addr指向bss

ret位置

shellcode/onegadget(需要先leak libc)/backdoor

执行rop链

rop(ret=pop rip)->栈溢出/栈迁移->指向rop链

Rop

先通过gadget控制寄存器,再调用依赖寄存器传参的函数/onegadget

Gadget1(pop Value到寄存器,ret=pop rip)

Value1

Value2

Gadget2(pop Value到寄存器,ret=pop rip)

Value3

Func(execve依赖寄存器/system@plt依赖寄存器和栈/onegadget依赖寄存器或无依赖)

Fake return addr(对于依赖栈的函数需要伪造栈结构,call=push rip)

Arg1

Arg2

栈溢出

通过栈溢出,从return addr处开始构造rop链,ret时rsp指向rop链

栈迁移

修改备份的帧指针,使其指向已经构造好的rop链,第一次leave时pop rbp,第二次leave时mov rsp,rbp,ret时rsp指向rop链

vsyscall

相当于ret,可以用来在rop链中占位

Ret2dl_resolve(解析libc任意函数地址)

Stage1:migrate

Rop1:padding+read(base)+migrate(base)

Rop2:write(str)+str

Stage2:migrate+dl_resolve

Rop1:padding+read(base)+migrate(base)

Rop2:plt0(str)+str

Stage3:migrate+dl_resolve+fake_index

Rop1:padding+read(base)+migrate(base)

Rop2:plt0(str)+fake_reloc->write_symbol@dynsym+str

Stage4:migrate+dl_resolve+fake_index+fake_symbol

Rop1:padding+read(base)+migrate(base)

Rop2:plt0(str)+fake_reloc->fake_symbol+fake_symbol->write_str@dynstr+str

Stage5:migrate+dl_resolve+fake_index+fake_symbol+fake_str(fake_str=’write’)

Rop1:padding+read(base)+migrate(base)

Rop2:plt0(str)+fake_reloc->fake_symbol+fake_symbol->fake_str+fake_str+str

Stage6:migrate+dl_resolve+fake_index+fake_symbol+fake_str(fake_str=’system’)

Rop1:padding+read(base)+migrate(base)

Rop2:plt0(str)+fake_reloc->fake_symbol+fake_symbol->fake_str+fake_str+str

总结:plt[0]->dl_resolve->index->reloc->symbol->str

plt0作用:执行dl_resolve来解析函数的地址并将地址填写到got,再执行got指向的函数
fake_reloc = flat([write_got, r_info])
write_got可以用原来的got,也在bss段随便找一个位置,只要对应位置可写就可以

ctf-wiki用的是原来的got,ROP_LEVEL5用的是bss+0x200

Heap基本知识

任意分配

Fastbin Double Free 是指 fastbin 的 chunk 可以被多次释放,因此可以在 fastbin 链表中存在多次。这样导致的后果是多次分配可以从 fastbin 链表中取出同一个堆块,相当于多个指针指向同一个堆块。如果更进一步修改 fd 指针,则能够实现任意地址分配 chunk。

House of Spirit 在目标位置处伪造 fastbin chunk,并将其释放,从而达到分配指定地址的 chunk 的目的。

当然,也存在直接UAF就可以修改fd指针的情况。

堆地址对齐0x10

Tcache

Lib>2.26:第一次malloc在申请目标chunk之前,会先申请一个大小为0x251的tcache_perthread_struct、输入缓冲区和输出缓冲区(后两个可以通过setbuf关闭)

chunk 分配位置

栈:控制返回地址等关键数据。

libc:使用字节错位来绕过 size 域的检验,实现直接分配 fastbin 到_malloc_hook的位置来控制程序流程。

与chunk有关的功能

堆在编辑的时候有溢出:直接溢出改fd

堆在新建的时候有溢出:先new多个chunk进行内存布局,再用free和new溢出改后面chunk的fd

堆无溢出:double free,第一次申请改fd

堆有溢出且程序有指针列表:Unlink修改指针列表,从而达到任意读写

Heap攻击流程

从漏洞成因的角度入手

悬挂指针/越界访问

从指针控制的角度入手

libc/got/ret/chunk指针列表/可读可写可操作可执行指针

指针操作

malloc(sz) 申请ptr,并修改*ptr

read(ptr) 输出*ptr

edit(ptr) 修改*ptr

free(ptr) 依据*ptr

①unsorted bin,修改*ptr=&libc

②fast bin改fd(堆溢出/悬挂指针:直接改或者间接用double free改),同时*fd满足一定条件,申请到ptr=fd

③unlink伪造chunk(堆溢出),同时存在ptr\'指向伪造chunk,修改ptr\'=&ptr\'-3

④先free掉再用malloc,申请到同一个ptr(应对malloc和edit绑定的情况)

⑤先free掉本来不存在的chunk再用malloc,申请到同一个ptr(首先要有可控指针)

从安全检测的角度入手

\'\0\',0,负数,有/无符号比较,整数溢出,数组溢出(off by one),alloc/free时堆块内容没清空,字符串末尾没截断

Main arena

#define NBINS 128
static struct malloc_state main_arena;
struct malloc_state {
mutex_t mutex;
int flags;
mfastbinptr      fastbins[NFASTBINS];
mchunkptr        top;
mchunkptr        last_remainder;
mchunkptr        bins[NBINS * 2 - 2];
unsigned int     binmap[BINMAPSIZE];
struct malloc_state *next;
INTERNAL_SIZE_T system_mem;
INTERNAL_SIZE_T max_system_mem;
};

Chunk分类

fast bins,unsorted bins,small bins,large bins

main_arena.bins存储各个表头chunk的fd和bk(双向链表),包含unsorted bins(1),small bins(2~63),large bins(64~127)

fastbins指向各个fast bins(单向链表)

main_arena.bins结构

bins[0]=bin1.fd bins[1]=bin1.bk
bins[2]=bin2.fd bins[3]=bin2.bk

后边的bins以此类推

Unsorted bin UAF

linux中使用free()进行内存释放时,不大于 max_fast (默认值为 64B)的 chunk 被释放后,首先会被放到 fast bins中,大于max_fast的chunk或者fast bins 中的空闲 chunk 合并后会被放入unsorted bin中(参考glibc内存管理ptmalloc源码分析一文)

而在fastbin为空时,unsortbin的fd和bk指向自身main_arena中(双向链表的表头位于main_arena.bins),该地址的相对偏移值存放在libc.so中,可以通过use after free后打印出main_arena的实际地址,结合偏移值从而得到libc的加载地址。

def offset_bin_main_arena(idx):
word_bytes = context.word_size / 8
offset = 4  # lock
offset += 4  # flags
offset += word_bytes * 10  # offset fastbin
offset += word_bytes * 2  # top,last_remainder
offset += idx * 2 * word_bytes  # idx
offset -= word_bytes * 2  # bin overlap(前面的prev_size和size)
return offset
unsortedbin_offset_main_arena = offset_bin_main_arena(0)
main_arena_addr = unsortedbin_addr - unsortedbin_offset_main_arena
libc_base = main_arena_addr - main_arena_offset(偏移值写在libc源码中)

House of Einherjar(堆溢出)

修改prev_size和P标志位->任意分配

House of Force(堆溢出,任意大小malloc)

修改top_chunk的size->任意分配

House of Orange(无free产生unsorted bin)

申请大于top chunk的chunk,会申请新的top chunk,原来的top chunk会被放入unsorted bin

Unlink(堆溢出)

修改prev_size、size的P位、fd和bk->修改某个指向chunk的指针指向其上面的地方->修改周围敏感数据及指针自身->任意写

P Q地址相邻,在P的Data区填写构造假chunk F,包含prev_size size fd bk和next chunk\'s prev_size(也可以用Q的prev_size),并覆写Q的prev_size和size.prev_inuse

从而使Free Q时让堆管理器认为F已经释放,于是把F Q合并,并将F从原来的链表unlink,又由于P的fd和bk是精心构造的,导致unlink的时候能够修改某个原本指向P的Data区(即F)的指针ptr重新指向&ptr-3,从而修改ptr周围的数据

修改前

ptr=&P.Data
(F.bk)->fd=(F.fd)->bk=&ptr

修改后

ptr=&ptr-3

堆地址对齐,空间复用,Unlink,off by one

堆地址对齐0x10,如果申请大小size%0x10大于0且不大于8,则申请的chunk和下个chunk的prev_size形成空间复用,从而在off by one的时候可以覆盖到下个chunk的size.prev_inuse,进而构造Unlink

Oreo(可控指针)

Str溢出造成指针可控->

任意读->泄露

任意释放->任意分配->任意写

控制next指针指向got表项,泄露libc base

控制next指针指向bss,释放假chunk(Spirit),从而申请到包含msg指针的chunk,使msg指针指向got表

控制got表项指向system,获得shell

str溢出->改ptr->leak libc

str溢出->改ptr->释放目标chunk->分配目标chunk->改ptr->改got->获得shell

Search Engine(悬挂指针)

悬挂指针->’\0’绕过检测->指针复用

unsorted bin UAF->泄露

fast bin UAF->程序无修改功能->Double free->任意写

利用 unsorted bin 地址泄漏 libc 基地址

利用 double free 构造 fastbin 循环链表

分配 chunk 到 malloc_hook 附近,修改malloc_hook 为 one_gadget

申请unsorted chunk(uc)->free->ptr没清零且校验可用\0绕过->leak libc

申请a b c,均含\'d\'

list:c->b->a->uc(已清零)->NULL

delete \'d\'

y(c)y(b)y(a)

0x70bin:a->b->c->NULL

list:c(已清零)->b(已清零)->a(已清零)->uc(已清零)->NULL

delete \'\0\'

(c的fd为NULL,所以初始字节为\0,没通过*i->sentence_ptr检验,不会提示,这也就是c的作用,否则b在这里就不会有删除的提示,就无法double free)

y(b)n(a)n(uc)

0x70bin:b->a->b->a->...(申请a是为了绕过相邻检测)

分配b并使fd指向_malloc_hook->分配a->分配b->分配chunk到_malloc_hook->改_malloc_hook指向one_gadget->获得shell

Wheelofrobots(悬挂指针)

悬挂指针->off by one绕过检测->double free->修改读入限制

堆溢出->unlink->可控指针->泄露libc->改got->获得shell

Roc826\'s_Note(悬挂指针)

堆内容没有初始化->泄露libc

悬挂指针->double free->修改got->获得shell

Babyheap(堆溢出)

堆溢出->

修改fd(和size)->任意分配->

任意读(程序删指针,需要构造两个指针unsorted bin UAF)->泄露

任意写

利用 unsorted bin 地址泄漏 libc 基地址。

利用 fastbin attack 将chunk 分配到 malloc_hook 附近。

利用0x1000对齐可以1/16爆破相邻地址

申请0x10大小的a@0x00 b@0x20 c@0x40 d@0x60和0x80大小的e@0x80

(a b c用来构造指向e的fd,d用来修改e的size,从而申请到e的第二个指针)

List:1(a) 2(b) 3(c) 4(d) 5(e)

释放3 2

Bin:b->c->null

填充a的str溢出->控制b的fd指向e

填充d的str溢出->控制e的size为0x21

分配2(b)->分配3(e)

List:1(a) 2(b) 3(e) 4(d) 5(e)

释放5->读取3->UAF泄露libc

同样的方法申请chunk到_malloc_hook->指向onegadget->malloc触发->获得shell

Stkof(堆溢出)

堆溢出->unlink->修改指针周围敏感数据(指针列表)->指针可控->任意写

Unlink修改指针global[2]=global-1

修改global[0]指向free@got->修改free@got指向puts@plt->构造leak

修改global[1]指向puts@got->调用free实则是puts->泄露libc

修改global[2]指向atoi@got->修改atoi@got指向system->获得shell

note2(堆溢出)

堆溢出->unlink->修改指针周围敏感数据(指针列表)->可控指针->任意写

新建node时Size填0导致size-1溢出无穷大,从而产生堆溢出

申请note的大小分别为0x80(a),0(b),0x80(c),完成内存布局

这里因为只有新建node时有漏洞,而编辑node时没有漏洞,所以需要先free掉,再利用new写进内容(由于bin的机制,free和new的chunk是同一个)

布局假chunk需要5个word,而b中只有2个word的空间,所以需要申请一个a来布局

a中布局假chunk,利用b溢出覆写c,free c触发unlink,ptr[0]=ptr-3

修改ptr[0]指向atoi@got->查看内容->泄露libc

修改atoi@got指向system->获得shell

Annevi_Note(堆溢出)

堆内容没有初始化->泄露libc

堆溢出->Unlink->可控指针->改got->获得shell

New 0(0xb0) 1(0xb0)

Edit 0 (p64(0)+p64(0xb0)+p64(list_addr-0x18)+p64(list_addr-0x10)).ljust(0xb0,\'\x00\')+p64(0xb0)+p64(0xc0)

Free 1->unlink

申请0xb0不会和下个chunk的prev_size空间复用

Annevi_Note2(堆溢出)

程序关闭标准输入输出,需要修改stdout为stderr,修改低两字节(4K对齐,1/16概率)

堆溢出->Unlink->可控指针

修改stdout为stderr->泄露libc

还原stdout和stdin

修改free_hook为system->获得shell

用可控指针修改指针列表时还没有泄露libc,所以修改的时候保留一个指针指向指针列表,等到泄露libc知道free_hook位置后,再用这个保留的指针修改指针列表,从而修改free_hook为system

E99p1ant_Note(堆溢出)

堆内容没有初始化->泄露libc(main_arena)

负数越界

show(-7)->泄露指针列表位置

show(-23)->泄露libc(IO_2_1_stdout)

堆溢出->Unlink->可控指针->改free_hook_addr->获得shell

new 0(0x98),1(0x88)

edit 0 (p64(0)+p64(0x90)+p64(list_addr-0x18)+p64(list_addr-0x10)).ljust(0x90,\'\x00\')+p64(0x90)+\'\x90\'(<-off by one@1)

free 1->unlink

list[0]=list_addr-0x18

edit 0 p64(0)*3+p64(free_hook_addr)+\'\n\'

list[0]=free_hook_addr

edit 0 system_addr

new 2 \'/bin/sh\0\'

del 2 ->获得shell

申请0x98的时候,最后的0x8会和下个chunk的prev_size空间复用,从而在off by one的时候可以覆盖下个chunk的size.prev_inuse

ROP_LEVEL5(栈溢出)

Ret2dl_resolve

解析并调用system

ROP_LEVEL2(栈溢出)

栈迁移+ORW(seccomp)

rop@bss布局

read stdin->file@bss

open file

read file

puts file

ret前栈布局

data

s->rop

r->leave<-rsp rbp=rop

执行leave;ret

rsp=rbp

rip=[rsp]=[rop]

执行rop链

read <-\'./flag\'

open \'./flag\'->4

read 4->buf

puts buf->flag

形而上的坏死(栈溢出)

任意读和任意写

Ret2main修改ret两个低字节(0x1000对齐,1/16概率),然后进行多次循环

改got为printf->泄露canary和libc

有无符号数比较->绕过读入限制->rop

Fys(linux文件系统)

利用Linux文件系统的inode来定位

参考资料

https://www.cnblogs.com/alisecurity/p/5486458.html

https://paper.seebug.org/1109/#14-unsafe_unlink

https://wiki.x10sec.org/pwn/heap/unlink/#_5

https://ctf-wiki.github.io/ctf-wiki/pwn/linux/glibc-heap/heap_overview-zh/