Structure
从高到低(来自ctf-wiki):
从低到高:
Low addr
-------------------------------------------------------
New stack
Data
Cannary
Saved registers
Return address
Old stack
Old chunk
New chunk
Prev size|Prev Data(Reuse)
Size(AMP)
Data|Fd
|Bk
Top chunk
-------------------------------------------------------
High addr
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。
与 thread 不同的是,main_arena 并不在申请的 heap 中,而是一个全局变量,在 libc.so 的数据段。
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->shellcodeonegadget(需要先leak libc)/backdoor
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链
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:plt[0](write_index@plt)(str)+str
Stage3:migrate+dl_resolve+fake_index
Rop1:padding+read(base)+migrate(base)
Rop2:plt[0](fake_index->fake_reloc)(str)+fake_reloc->write_symbol@dynsym+str
Stage4:migrate+dl_resolve+fake_index+fake_symbol
Rop1:padding+read(base)+migrate(base)
Rop2:plt[0](fake_index->fake_reloc)(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:plt[0](fake_index->fake_reloc)(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:plt[0](fake_index->fake_reloc)(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 的目的。
Lib>2.26:第一次malloc在申请目标chunk之前,会先申请一个大小为0x251的tcache_perthread_struct、输入缓冲区和输出缓冲区(后两个可以通过setbuf关闭)
chunk 分配位置:
栈:控制返回地址等关键数据。
libc:使用字节错位来绕过 size 域的检验,实现直接分配 fastbin 到_malloc_hook的位置来控制程序流程。
改fd从而任意地址分配chunk,从而达到任意读写(unsorted bin UAF泄露libc):
堆有编辑溢出:直接溢出改fd
堆有新建溢出:先new进行内存布局,再用free和new溢出改fd
堆无溢出:double free,第一次申请改fd
堆有溢出且程序有指针列表:Unlink修改指针列表,从而达到任意读写
Heap攻击流程(堆溢出&没指针):
①悬挂指针
UAF(unsorted bin)->泄露libc
Double free(fast bin)->任意地址分配chunk->③可控指针(有约束)
②堆溢出
存在全局指针
Unlink->③可控指针(有约束)
修改fd
任意地址分配chunk->③可控指针(有约束)
提前计算堆地址构造第二个指针->①悬挂指针
③可控指针
已泄露libc
修改_malloc_hook(字节错位)->④获得shell
修改got->④获得shell
未泄露libc
读取got->泄露libc
修改关键数据
修改指针->③可控指针(无约束)
为绕过校验提供条件->②堆溢出
④获得shell
-------------------------------------------
指针流Heap攻击:
程序中自带ptr,got=&libc,ret=&libc
malloc(sz) 申请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(首先要有可控指针)
read(ptr) 输出*ptr
edit(ptr) 修改*ptr
绕过检测:'\0',0,负数,有无符号比较,整数溢出,数组溢出(off by one)
-------------------------------------------
Ⅰ泄露libc
Ⅱ获得shell
Main arena
#define NBINS 128
static struct malloc_state main_arena;
struct malloc_state {
mutex_t mutex;/* Serialize access. */
int flags;/* Flags (formerly in max_fast). */
mfastbinptr fastbins[NFASTBINS];/* Fastbins */
/* Base of the topmost chunk -- not otherwise kept in a bin */
mchunkptr top;
/* The remainder from the most recent split of a small request */
mchunkptr last_remainder;
mchunkptr bins[NBINS * 2 - 2];/* Normal bins packed as described above */
unsigned int binmap[BINMAPSIZE];/* Bitmap of bins */
struct malloc_state *next;/* Linked list */
/* Memory allocated from the system in this arena. */
INTERNAL_SIZE_T system_mem;
INTERNAL_SIZE_T max_system_mem;
};
typedef struct malloc_chunk *mfastbinptr;
mfastbinptr fastbinsY[ NFASTBINS ];
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)
fastbinsY指向各个fast bins(单向链表)
main_arena.bins结构:
bins[0]=bin1.fd bins[1]=bin1.bk
bins[2]=bin2.fdbins[3]=bin2.bk
后边的bin以此类推
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源码中)
Einherjar(堆溢出)
堆溢出
修改prev_size和P标志位->任意分配
Force(堆溢出,任意大小malloc)
堆溢出
修改top_chunk的size->任意分配
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(由于地址复用size=next chunk's prev_size+1),并覆写Q的prev_size和size的P
从而使Free Q时让管理器认为F已经释放,于是把F Q合并,并将F从原来的链表unlink,又由于P的fd和bk是精心构造的,导致unlink的时候能够修改某个原本指向P的Data区(即F)的指针ptr重新指向&ptr-3,从而修改ptr周围的数据
修改前:ptr=&P.Data
修改后:ptr=&ptr-3
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 附近。
利用4K(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
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,1
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
ROP_LEVEL5(ROP)
Ret2dl_resolve
解析并调用system
ROP_LEVEL2(ROP)
栈迁移+ORW
seccomp:orw
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
形而上的坏死(ROP)
任意读和任意写
Ret2main修改ret两个低字节(4K对齐,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/