这个视频讲的比较难,我花了比较长时间来分析,甚至一个点反复很多次,这也是在学PWN的过程中不可避免的,需要坚持和毅力
pwn3:
没有system,通过ROP调用write的plt入口,执行write函数,并且将gots里的内容打印出来,泄露libc
远程libc和本地不同。有些大的版本更新会使libc发生大变,所以不能用一个脚本。
改成本地。用一下命令可以看到libc的路径
so.6只是软连接,通过以下可以获得真实路径
这一题是真正需要我们自己泄露内容的题目。这就需要泄露的过程,但是一次不能把ROP链构造完。
先一次构造完试一下,在构造payload时后面应该加上system地址,但是现在并不知道执行完write返回的system地址。其实这里就可以填vulnerable_func函数的地址,这样就会在执行完后返回read造成栈溢出
再通过payload,重新执行write,打印出想要的内容,程序再执行一次vulnerable_func函数
相当于程序执行流被我们劫持了,我们可以去到已知的地址任意一个地方,一会会返回这个函数,还有一次溢出。
下一步加write参数(三个),elf got表里的write对应表象。第一个参数在低地址,后面在高地址
这是第一次栈溢出,并且执行完之后会重新返回到有漏洞的地方,为了下一次溢出
到这里,我猜前段是为了在write写入write的libc地址,然后让程序自己把libc真实地址泄露出来
补充:
p32函数是将整数转换为字节型数据,可以用u32逆转换,再转为16进制。这就是write在内存中的真实地址。p是pack打包,u是unpack解包
继续找system和bin/sh的地址。这两个都在libc中,要得到基地址,是用write真实地址-write在libc中的偏移。那么system的地址就是libc的基地址+system在libc中的偏移;bin/sh就是libc的基地址+第一个bin/sh在libc中的偏移
这是第一个payload,执行完后会回到vulnerable_func函数,这就要再次构造payload
就可以获得shell
花式栈溢出:
栈中有很多栈帧,每个栈帧对应一个函数的执行状态。但是在栈中所有的内容不都是栈帧,第一个是main函数,在其之前没有栈帧,在这之前保存了什么内容?
找一道题,checksec,一个动态链接的程序
复习:
可以直接带到这个ida中
白色背景直接存在于程序中,而粉红色只存在于plt表象
start函数用于布置环境,没有C语言;栈在栈帧之外存在的内容,如当前执行程序的名字
由于动态链接库,libc start main函数先来到plt,第一次调用,先解析真实地址
ret到start函数末尾
这道题没有说main函数,但是可以在ida中找到,地址4006D0
这题带有Canary
这道题需要溢出的数据很大,得几百
侦测到了缓冲区溢出,但是输出的内容不一样,就是因为打开了Canary保护
没有这个保护,就直接向上溢出
虽然也是可以直接覆盖,但是返回时先检查Canary,如果和最开始放的值不同,就会触发stack chk fail函数,输出一些东西并且强行退出程序
补充 canary:
如何在ida里判断是否开启canary保护?
黄色区域这一行代码就是对应的canary
对应汇编实现
把一个十六进制数mov到rax,再移到栈上。就是放置canary,在v4
最后原来canary的随机数会和v4异或,值得是0,就是每一位都相等
汇编里有一个jnz指令,在之前的博客里讲过
例题(smashes):
其实这道题就是上面分析的,ctrl+shifft+12,好像又一个flag
flag的意思是服务器上的flag所占用的位置
返回到原代码
第10行写入数据,第14行输出一行“你好,请复写flag”,第17行读取输入,如果什么都没有输入,会到LABEL_9,强行退出程序;如果是换行符,就会跳出程序。第22行存的数据就是之前flag内容,这会导致我们输入的内容将服务器上真正的flag给复写掉。如果是跳出程序,之后也会将flag清零。
就是说如果按照它的程序是肯定拿不到flag
补充:elf很小的时候,可以在编译的时候通过某些选项,elf中的某些节和段会在虚拟内存中映射两份
在600D40有一份,在0X400d20这里也有一份
data段被映射到了这两个位置,就是即使600被复写了,400还会有,写的内容一样
按理说应该不能栈溢出,但是已经写好的payload里有一次
补充:这里的main函数没有被特殊符号标记,使用了strip,使gdb辨别不出来有main函数。需要通过ida里函数地址
这里可能还有其它保护,致使没法调试,这里老师遇到了点问题。可能是这道题的libc太老,而新的会把漏洞给修复掉
还是要学习一下这道题的知识
这里变成了0,应该就是漏洞被修复的位置
这行应该是程序名,所对应的字符串的地址,向上溢出到canary,触发造成程序强行终止,在终止前会将程序名称打印出来。然后取这个地址,到这个地方寻找程序绝对路径名称的字符串。把这个指向路径名的地址篡改成指向flag的地址,报错绝对路径名,会把flag的值当作程序路径名打印出来
栈迁移:
会碰到下面情况
长度不够:到prev ebp就结束了
思路:把这个栈的内存区域转移到另一个内存区域
只覆盖ebp,还能劫持执行流。汇编后面两个指令leave和ret,leave就是相当于mov esp,ebp和pop ebp,之后esp继续抬高一个字长,ebp就会指向刚开辟的可控制的区域,就不在原来的栈里
第二个子函数后面也是这样
先用ppt展示一下
最简单的情况就是溢出的并不只是一个长度(不只ebp),还能向上溢出一些距离。任务就是要把栈迁移到另一个区域,控制了栈的两个寄存器ebp,esp,就可以欺骗操作系统,控制到另一个区域。另一个区域有我们设计的gadget,能完成一次完整的攻击
怎样迁移呢?会有一个函数栈溢出,覆盖上面的内容。函数最后使leave,ret,会将ebp篡改为想要的值。进行溢出时会pop ebp到恶意覆盖的地址,还有esp。就需要依靠返回地址创造的gadget,可以leave或pop,将esp移到ebp。可以在新的空间内构造数据完成攻击流程
这里可以发现ebp帮助esp标记了一个返回的位置,而且数据读写都在栈底。就是说不管怎样,只要把esp控制了就行
大体是这样,其实有很多种,比这难,听着很乏味,需要看具体题目
例题 PWN3 X64:
拖入ida
跟上一题逻辑,漏洞一样,就是变成了64位
构造两次ROP,调用write,打印write的plt地址,再返回这里,再溢出,ROP,调用system,bin/sh。就是构造payload时传参会有不同,参数用寄存器来存放
payload:
第一个payload调用write,并写入got表。从ret addr开始,写入三个参数,rdi,rsi,rdx,需要相应的gadget控制。
找到了rdi;rsi是r15 ret,写入write got表地址,pop r15要写入一些垃圾数据;第三个参数是向屏幕输出的参数的最大长度,没有rdx,只能赌运气,就是rdx的值>8。回到vulner func
第二个payload,先rdi ret,然后bin/sh,最后system
格式化字符串漏洞:
以一个代码为例
对于这个printf,输出一些参数,%加数字加字母,这个就是格式化字符串
后面的一些给删去,还是会输出,就是printf函数的缺陷
有时候后方并没有检查格式化参数数量和printf后方接收到的参数数量是否一样
补充:
这个命令是打开脚本的调试模式
调试运行一下,其中gcc -g可以带着源码
会显示发送的数据和接受的数据,还按字长对齐了。每一步很清楚
调试一下上面代码,这里出问题了,另举例
printf输出时,前面有几个%x,后面有几个参数,最好是整型,那么就会按照十六进制格式打印出来
但是把后面参数去掉,会打印出来一些奇怪的数据,对照一下
再没有给任何参数的情况下,直接把栈上的内容打印了出来
这是为什么呢?在X86的情况下,printf栈帧,先向上数2个字长(ebp,ret addr),接着找自己的参数,例如%x%x%x%x,要向上找四个字长,就会跳过那两个字长,向上四个字长的值作为参数,而这四个字长应该是由父函数放置(子函数的参数由父函数压入)。
在call时如果后面没有参数(a,b,c,d),高地址就没有这四个值,但是父函数的内容还在。还是会向上找四个参数,而这现在就是父函数的内容,就会把父函数的一些地址给泄露出来。这是格式化字符串的基本原理
后面会更加深入的讲格式化字符串,再往后就会讲堆。