堆栈攻击基础
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(高地址)&(低地址) | ||
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环境下程序的加载过程
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/