0%

栈溢出总结

栈溢出总结

因为我是师从ctf-wiki,所以大纲大概会和ctf-wiki重复,不过这个是个人理解的总结,如有错误,请大佬指点

基础ROP

ret2text

text就是程序本身已有的代码段,以前是直接写个后门函数,然后让你ret2func,直接返回到那个指定函数处就可以了。不过他的思想还是很不错的,比如说

1
2
3
4
5
6
7
void func(){
if(result == 1){
system("/bin/sh");
}else{
printf("Too young too simple");
}
}

比如这句,我们不一定得返回到func函数开头位置,我们可以直接利用已有代码段,假设system(“/bin/sh”)这句话在0x08048a5这个地址处,我们直接覆盖将返回地址覆盖成这个地址就行了,而不需要从头开始执行

利用:

  • 通常这种方法无泄露的话,要求pie保护没开
  • 有泄露的话,看情况,可以ret2libc,如果能泄露pie基地址,也可以利用这种方法

ret2shellcode

这个就是返回到shellcode处,最开始学的时候就做了几道题,后面就没遇到过它了。

这种先看保护,无nx的话,首先考虑一下他,因为冯诺依曼结构是数据跟代码放在一起,而这个思路就是将数据当作代码来执行,比如bss段有个数组,我们往里面写了一段代码,然后我们ret到这个数组处,就可以执行这段代码了,前提是这个数组有可执行权限的

还有就是通过jmp esp这个操作,执行shellcode,首先布置shellcode,通过移动esp,然后jmp esp,执行shellcode,这个思路也是很强的

ret2syscall

这个就是通过ROP执行系统调用,原理是一样的,

execve(“/bin/sh”,NULL,NULL)

  • eax=0xb
  • ebx为/bin/sh地址
  • ecx=0
  • edx=0

通过pop eax,pop ebx这些,设置好寄存器后,在通过int 80执行拿shell,这种在写shellcode的时候这些系统调用号以及参数经常得用上

ret2libc

大部分题目都是这种情况,因为最经典了,通过泄露libc函数地址,然后查找libc版本,然后执行一系列的操作,至于原理,因为libc有个symbols,每个函数的偏移都记录下来了,而且每个版本对应的偏移是确定的,所以我们知道其中一个函数的地址的话,减去偏移,得到的地址为基地址,通过这个基地址加上symbols表里的偏移就得到该函数的地址了

查找libc版本有LibcSearcher, libc-database ,大部分都是从libc-database演变的,所以还是得有libc-database那个库

中级rop

ret2csu

这个可以说是万能的?利用 __libc_csu_init这里的东西,这个函数通常对libc做初始化操作的

1
2
3
pop rbx; pop rbp; pop r12; pop r13; pop r14; pop r15; retn #1
mov rdx, r13; mov rsi, r14; mov edi, r15d; call qword ptr [r12+rbx*8] #2
pop rdi; retn # 通过对pop r15的地址做+1偏移就得到他了

从这里我们可以看到能控制很多寄存器,rbx,rbp,r12,r13,r14,r15,rdx.rsi,edi,rdi
通常我们64位用到前4个就行rdi, rsi, rdx, rcx
如果找不到gadget,就用这个进行构造ROP链条

注意,由于我们这里需要覆盖rbx=0,从而是r12+8*rbx=r12这样就直接call r12了,不然我们会容易导致这里执行错误,还有就是要设置rbp=1,因为执行过后有add rbx,1,让他相等,也就是让这个jnz不跳,我们就可以往下继续执行了

执行过程通常是先执行1,设置r13,r14,r15这些参数,然后执行2里的,call完过后我们需要填充一些空字节,因为有大量的pop,提升了栈顶

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
pop_rbx_rbp_r12_r13_r14_r15 = 0xa
rdx_rsi_edi_call = 0x0

def csu_set(offset, edi,rsi,rdx):
payload = flat([
'a'*offset,
pop_rbx_rbp_r12_r13_r14_r15,
0, # rbx
1, # rbp 为后面做准备
got函数, # 函数地址 r12
rdx, # r13
rsi, # r14
edi, # r15
rdx_rsi_edi_call, # rdx=r13, rsi=r14,edi=r15,r12= call地址
p64(0)*7, # 填充pop
func # 指定函数
])

大概就这样,可以设置三个参数,同时还可以通过偏移设置,就是将地址偏移下,就得到了一个新的指令,这是个很强的思路,构造syscall有时候也会用上这个

ret2reg

这个呢,怎么说很强吧,虽然我用过这个思路,但是我用的时候都是错的,hh,这个就是假设寄存器存了一个mmap的地址,然后这mmap空间里我们有自己写了一段shellcode,我们jmp到这个寄存器就可以执行shellcode了

brop

这种情况在做题中几乎不会遇到,但在现实中比较有可能,因为brop需要的时间相对比较长,而且相对比较麻烦

  1. 源程序必须存在栈溢出漏洞,以便于攻击者可以控制程序流程。
  2. 服务器端的进程在崩溃之后会重新启动,并且重新启动的进程的地址与先前的地址一样(这也就是说即使程序有 ASLR 保护,但是其只是在程序最初启动的时候有效果)。目前 nginx, MySQL, Apache, OpenSSH 等服务器应用都是符合这种特性的。

这是攻击条件,ctf-wiki摘抄下来的

具体就是一步步爆破,模板可以照着ctf-wiki来,最终得到一个完整的exp拿shell

2018湖湘杯出了一道这个题,然后0解。。。所以大概率出题是不会这样出的,具体遇到在自行利用

高级ROP

其实高级rop是最简单的,原理不简单,利用简单,具体参照我这篇文章

SROP建议看i春秋Linux+pwn零基础入门教程,挺不错的,讲的都挺好

也可以看ctf-wiki部分

花式栈溢出技巧

大部分的ROP其实都掺杂了这个,因为现在ctf门槛也高了,出个栈溢出也不能出简单了,出简单就没兴趣了大家,比手速而已,掺杂各种技巧,绕过,最后拿到shell

stack pivoting

  1. 这个通过控制sp指针的利用,比如ret2shellcode,可以往数组里写shellcode,然后通过sub esp,数值 jmp esp执行
  2. 还有就是通过滑板,比如主函数,有read(0, buf,0x100),然后子函数里有个for循环复制这个buf数组,最多也就能覆盖到ret处,这时候的限制就很多了,只能调用一个地址,然后这个地址我们可以设置为libc_init那个地址,因为他有大量的pop,通过滑板,我们滑到主函数的buf里,我们就可以执行我们的rop了。在或者利用add rsp,0x28这种,有的话也可以用,让他提高栈帧,原理是一样的

栈迁移

就是将栈转移到bss段或者堆上,这种技巧经常用到,如果不会这种技巧很吃亏

简单来说就是我们劫持栈帧,移动栈帧到bss段或者别的地方

劫持原理:

一个函数开头通常是

1
2
push ebp
mov ebp,esp

尾巴通常是

1
2
3
mov esp,ebp
pop ebp
retn

或者

1
2
leave #相当于mov esp,ebp ; pop ebp
retn

这里需要两次leave retn 至于为什么呢,原因可以看ctf-wiki,也可以自行调试

大概就是

  1. 第一次leave 是将rbp/ebp 变成我们覆盖的rbp而已,第一次只有pop ebp,或者pop rbp有用而已
  2. 第二次是mov esp,ebp也就是将我们的假栈帧变成esp,然后在pop ebp,这样要注意的是最后有个pop,所以应该是从buf+4开始执行,buf设置为填充就行

然后开始执行我们的ROP

特点: 输出长度不够

stack smash

这个没什么好说的,实际上就是通过覆盖 libc_argv[0] 这个参数,然后stack_check的时候报错会输出这个,所以可以泄露内存中的东西

partial_write

这个也是经常用的办法,通过覆盖一部分地址,达到绕过的,通常需要爆破,比如在栈上,我们通常需要两次ROP,才能拿shell,第一次泄露,第二次拿shell,而第一次泄露不知道返回地址怎么改的时候,可以尝试着修改后三位,在堆上这也是一种常用的办法,比如通过unsortedbin得到一个main_arena+88的地址,然后通过覆盖其后三位可能可以修改到stdout的base部分,然后通过IO_file攻击泄露,这个思路适用于很多地方

bypass pie

先推一波文章吧,这篇我觉得总结的很好i春秋大佬的

  1. partial_write

因为即使开启了pie保护,后1.5个字节是不变的,所以通过修改这后1.5个字节来绕过pie保护

  1. 泄露地址bypass pie

这个很常用,如果能泄露pie基地址的话,ROP链条都可以用了

  1. vdso/vsyscall bypass PIE

这个我觉得不常用,最主要内核现在版本高了,这个适用于老版本的

bypass canary

  1. 格式化字符串泄露

常用思路不否认

  1. 通过数学计算

这个现在最常见的了,有个排序函数什么的,通过排序函数计算,或者泄露,有些可以用scanf绕过泄露,因为scanf输入+,-不会写入数据,然而又占用一个位置

  1. 劫持stack_check_fail的got表,然后报错的时候就可以执行指定代码了

  2. 覆盖tls里的canary和栈里的canary,同时覆盖让其对比成功,绕过

  3. 栈溢出泄露

通过二次覆盖,第一次覆盖canary的00字节,然后泄露出来,在覆盖回去就好了

  1. 爆破canary

这有要求的,利用子进程崩溃主进程不会崩溃,然后同一个进程内所有canary一样的,所以子进程存在栈溢出,可以爆破canary

  1. 这个利用ssp leak

p &environ 查找环境变量地址

p __libc_argv[0] 查看参数地址

bypass nx

用ROP技术就能绕过NX保护了

总结整体思路

  1. 看保护
    RELRO: got表的保护,如果开了的话,无法写got表,没开考虑got表
    Canary: 栈溢出保护,开了的话,想办法利用bypass canary绕过,没开直接ROP
    NX: 开了的话,利用ROP技术绕过,没开的话,考虑执行shellcode
    PIE: 开了的话就用bypass pie技术绕过, 没开的话,地址是固定的,考虑一下是否存在后门函数,查看是否有/bin/sh以及system函数

  2. 看字符串

经过第一层保护的筛选过后,大体有个思路,然后通过查看字符串,有没有什么关键点,比如出现了flag,考虑一下orw,open,read,write 的shellcode读出flag位置
出现了system,看有没有地方调用他,出现了/bin/sh,有没有地方调用

  1. 栈溢出查找
  • 存在栈溢出,查看长度,长度不够,考虑stack pivoting,以及栈迁移,在加上ret2reg,通过寄存器跳转
  • 长度够,保护有可以考虑partial_write

接下来就是从基础ROP开始一层层往上筛选

  • ret2text 判断是否有后门函数,或者system(“/bin/sh”) 或者 open(“flag”),若无转下一步
  • ret2shellcode 从保护里已经可以大概猜出了,有nx通常可以过这一部分了,还有别的情况,比如利用mprotect绕过
  • ret2syscall 利用ROPgadget查看是否有int 80,syscall以及别的,pop eax等gadget是否存在,存在考虑,不存在转下一步
  • ret2libc 大部分题目其实可以直接从这里开始考虑,因为这是最经典的方法,不过忽略了前几个,可能就变复杂了

这里面如果存在ROP无想要的gadget情况,可以考虑中级ROP的ret2csu了,通过这个来构造想要的参数

如果都不符合的话,比如无泄露,无libc,接下来先不考虑高级ROP,先考虑组合洞,比如是否存在格式化字符串,整数溢出,等等别的洞

如果还不行考虑高级ROP了,ret2dl_resolve,以及SROP了

  1. 如果以上都不符合

比如没有二进制文件,直接就上BROP了,不用考虑了,在比如,根本不是栈溢出,看到经典菜单,考虑堆了,堆中其实可能存在栈溢出的,因为出题人考虑不周到,可能不小心比如read函数,写栈里数据,然后在复制到堆上的,这时候其实也可以ROP,不过通常得搭配整数溢出,这样长度才无限制

总结

  1. 这篇文章是我总结的思路性提纲,方便做题的时候断了思路重新接上,若还有一些细节性的东西,我会继续补充
  2. 大部分都是文字,枯燥无味,没有图片什么的附加说明,所以别见怪

参考文章

i春秋linux+pwn零基础入门
ctf-wiki

本文作者:NoOne
本文地址https://noonegroup.xyz/posts/2e87f359/
版权声明:转载请注明出处!