0.浅谈一下这次的虎符杯
虎符结束了这么久就才开始写这篇博客实在有些抱歉,这题babygame说实话难度确实不大,而且我当时也绕过了伪随机数来到有格式化字符串漏洞的那个函数了。但是由于我对hijack retaddr这个方法的了解不足,导致最终没有打通,实在是可惜。在赛后看了很多大佬的wp也一直卡在这个点,这也反映出了我这块的薄弱之处。但经过了资料查阅、动态调试等一系列繁琐的过程后,我也算是吃透了这一题,故这篇博客我想详细谈一谈这题的解法。
1.再谈格式化字符串漏洞利用中的内存任意写
先看一下%n这个格式化字符串在printf函数中的用法:
1 | printf("%256c%n",addr); |
%n的作用是往addr里写入一个整数,这个整数就是前面格式化字符串中已经打印的字符个数,一般配合%c使用,且写入的整数占4字节。
%hn和%hhn的作用与%n类似,只不过写入的大小分别为2、1字节,因此超出的部分就会被抛弃。
像上面的三个语句达成的效果分别是,addr处的值被改为0x00000100(4 bytes)、0x0100(2 bytes)、0x00(1 byte)。且高位不会被覆盖,例如,假设第二个语句中addr处的值为0xffffffff,那么执行后的效果就是被改为0xffff0100。
并且,和用格式化字符串漏洞泄露地址类似,也可以和定位符配合来改变栈上指定的参数。例如,%8$hhn就可以局部覆写参数列表里第八个参数的最后一个字节,也就是64位栈上第2个参数的(栈上的参数在pwndbg里看是从第0个也就是格式化字符开始算起的)。
那么,如何利用这个达到任意内存地址写呢?(64位)
假设我们知道了某个地址addr,我们要改写这个地址上的数据,我们就可以利用 “%[num]c%k$n”+padding+p64(addr) 这个方法来改写addr处的值。其中num是要往addr上写的数,k为addr在参数列表的位置(这个要利用gdb动态调试可以获得。如果熟练的话也看的出来:比如p64(addr)前面有16个字节的话,那么addr就是栈上的第2个参数,也是参数列表里第8个参数。),padding用来补足对齐栈。
2.HFCTF2022 babygame
从主函数可以看出存在有栈溢出漏洞:
game函数是一个猜拳游戏,由伪随机数生成石头、剪刀或布:
vuln函数中存在格式化字符串漏洞:
保护全开:
因此我们的思路是先利用主函数的栈溢出漏洞覆盖随机数种子,使得生成的随机数可预测可控,以此来绕过game达到vuln;并覆盖canary的00来泄露canary和rbp。再利用vuln的格式化字符串;漏洞泄露程序基地址和libc基地址,通过hijack retaddr方法局部覆写返回地址后几位,最后返回到主函数上再次利用栈溢出漏洞ret2libc即可。
第一个难点是如何找到vuln的返回地址:
首先,vuln的返回地址肯定保存在栈上,并且肯定在主函数buf往前8个字节的位置。因为我们可以看出buf在主函数栈帧的低地址:
由函数栈的性质我们可以知道:由于vuln函数是主函数调用的,调用时会开辟栈帧,栈又是由高地址向低地址增长的,因此vuln函数的栈帧肯定是主函数栈帧的低地址相邻处,而开辟栈时最先做的就是将rip压栈,因此:主函数buf往低地址8字节处就是vuln返回地址存放的栈地址。
那么,这个地址要怎么找到呢?我们知道虽然栈的地址变化很大,但是相对地址是不会变的。因此,我们需要泄露rbp来计算相对地址,从而找到其物理地址。
我们用pwndbg进行动态调试,在printf函数入口处下断点,运行,找到buf写入处,查看附近的内存布局,可以找到压入栈中的rbp:
可以计算出到存放主函数调用函数返回地址的地址处的偏移:
第二个难点是寻找传入地址参数时它在栈上的位置:
这个其实理解后就不难找了。一开始我也不太理解为什么是用 %8$hhn ,而不是其他什么数字。
我们之前提过,用 %[num]c%k$n”+padding+p64(addr) 这个方法可以修改addr处存放的值,那么关键就是找到p64(addr)这个存放在了栈上的哪个参数位置。我们知道,64位程序栈上的参数是8字节对齐的,我们在格式化字符串漏洞处下断点,传入“aaaaaaaabbbbbbbbcccccccc”可以发现八个abc刚好对应了栈上第0、1、2个参数:
也就是说p64(addr)在参数列表上的位置取决于前面的字符串长度,如果前面是16字节,那么就是参数列表上的第8个参数。这样提醒我们在利用格式化字符串漏洞进行任意内存写的时候要注意栈对齐。
由于第一次我们要利用两次的格式化字符串漏洞,所以第一次我们要局部覆写返回到call vuln,由于这两个地址只有最后一个字节不同,因此我们只需要覆盖最后一个字节就行:
而第二次我们要局部覆写返回到主函数:
(注:这里我选择返回到偏移为0x14aa的地方,因为返回到主函数开头的话会报错。)
这和vuln的返回地址有一个半字节不同,我们可以采用爆破半字节的方式————但是这看运气,所以我这里采用一种比较保险的方法:
获取我们要到的地方的物理地址,与0xffff做按位与运算,这样我们就获取了地址的后四位,局部覆写两个字节就行。
最后再利用主函数的栈溢出漏洞ret2libc就行了。(官方给出的wp是返回到one_gadget,但我没试过。)
伪随机数的绕过前面一篇博客的一题re有提到过,这边就不赘述了,简单讲用内置的ctypes就能解决,非常简单。
泄露libc和程序基地址的方法也不赘述了,之前深育杯的那篇博客有写过。
完整的exp如下:
1 | from pwn import * |
成功获取了shell: