数字中国keep_stack'sWP以及pwntools和缓冲区相关知识
前言
其实neise1没去打这个比赛,主要是知道这个比赛的时候,报名已经截止了[遗憾],不过群里有人参加了,拿了这个题来问我,一开始都以为开了影栈和IBT,一直做不出来,绞尽脑汁也完全没有任何思路,绝望之下群友跟比赛大佬求来的exp后发现,远程环境根本没开这俩保护![绝望],那这题其实就是一个利用起来稍微麻烦一些的栈溢出,还是不难做的,不过从大佬的exp中学到了很多,所以写了这篇wp。
exp改自雾島风起時这里,neise1这里直接挂的大佬博客主页,可以去看一下。
分析
依旧先checksec一下。
虽然程序这里确实标着这两个保护,但估计是远程那边硬件不支持,所以实际上打远程的时候是没有的。
所以保护非常简单,我们接下来看ida。
这个就是主要函数了。
其中,sub_4014A8这个函数纯是用来进行混淆的,行为萝莉特别复杂,但是返回值完全没有被用到,所以咱们直接看后面几个函数。
neise1感觉这几个函数放在一起比较好讲,索性一次性全贴出来了。
前两个函数,主要行为是对一个全局变量进行了一系列运算,这个变量初始是0,经过两个函数运算后变为2,随后进入第三个函数的判断,如果为2,则v1等于4。这里的v1实际上是接下来函数的数据读取行为的步长,而这里就是程序的主要漏洞点。
因为答应了群友要写“小白也能看懂的”wp,所以这里会带着分析一下这个函数最复杂的部分,也就是for循环的第三个参数,我们这里一层一层看。
*(_QWORD *)(a1 + 136)
第一层,好的,这里面a1就是外层函数的s数组(注意,单纯的数组名是被视为指针的),但是由于传进这个函数时,该指针被视为了_int64类型的数据,所以这里需要用(_QWORD ),转为指向8字节数据的指针类型,再用符号解包,所以这部分的意思是,把距离s数组头,136字节(因为加法运算时a1还是整数类型,所以是地址直接+136而不是根据指针类型加)处的8字节数据取出。
(*(_QWORD *)(a1 + 136))++
这里的下一步操作就是自增了,这种加号在后的格式,说明它是先进行了下一步操作,再自增1的。
i * (*(_QWORD *)(a1 + 136))++
再外层就是乘i了,这里i=v1也就是4。
(i * (*(_QWORD *)(a1 + 136))++ + a1)
这一步,加上a1,也就是s数组的指针(地址)。
*(_WORD )(i * ((_QWORD *)(a1 + 136))++ + a1)
这一步,把加完之后的数视为2字节数据指针,并解包。
*(_WORD )(i * ((_QWORD *)(a1 + 136))++ + a1) = s
最后,把该函数内的2字节整数s的值放到指针指向的位置。
由后续程序得,这其实就是read读到的数据。
所以,实际上*(_QWORD *)(a1 + 136)得出的数,就是程序往s数组写入的下标idx,每次写入2字节,步长为i,是四字节。
而s数组长128字节(0x80),但函数限制的idx只要<0x80,所以我们实际能写到的范围是0x80*4=0x200,所以这实际上就是一个稍微麻烦一些的栈溢出。
利用思路
虽然我们只能每隔4字节写2字节,但是s数组所处栈帧的返回地址本身也是程序地址,所以我们只需要改2字节就能让程序返回main函数调用sub_4016c8这里,而再次进入该程序后,全局变量再次经过运算就不是2了,那么步长就会变为2,我们就能进行连续地写入,正常的构造利用链了。
值得注意的是idx本身也在我们可写的范围内,所以我们可以直接修改idx让函数直接跳过去写返回地址。
exp
1 | from pwn import * |
pwntools
这个exp让我学到了很多新知识,所以稍微讲一下,首先是一些好用的基础设置。
1 | context.timeout = 5 |
这个是给所有交互函数设置的全局超时阈值,也就是等待超过5秒,就不等了,继续运行后续代码。建议在调试的时候不要开(别问我怎么知道的)。
1 | BASE = os.path.dirname(os.path.abspath(__file__)) |
这几行是为了让脚本不管在哪运行,都能找到当前脚本所在文件夹里的靶机文件
__file__:当前脚本文件的路径
os.path.abspath(file):获取脚本完整绝对路径
os.path.dirname(…):提取脚本所在的文件夹路径,存入 BASE
os.path.join(BASE, xxx):拼接出:
BIN:靶机可执行文件 keep_stack 的完整路径
LIBC:同目录下 libc.so.6 的完整路径
LD:同目录下动态链接器 ld-linux-x86-64.so.2 的完整路径
- process([LD, ‘–library-path’, BASE, BIN])
LD:手动指定用我们本地的 ld-linux-x86-64.so.2 动态链接器启动程序
–library-path BASE:告诉链接器,优先在 BASE 目录下找 libc 等依赖库
BIN:要启动的靶机程序
- env={‘GLIBC_TUNABLES’: ‘glibc.cpu.hwcaps=-IBT,-SHSTK’}
这个用来关闭保护
-SHSTK:关闭影栈(Shadow Stack)保护
-IBT:关闭间接分支跟踪(Indirect Branch Tracking)
缓冲区相关知识
为什么聊这个呢,是因为我发现exp中第一次发送的数据,开头是b’65535X’,我不理解这个X是干什么用的,scanf读取完65535之后,这难道不会被后面的read读取么?但是我用gdb调试了一下发现,确实没读取,read直接读取了AA,所以我就研究了一番。接下来让我解释一下。
首先,缓冲区分为
应用层:stdio流缓冲区
系统层:操作系统内核缓冲区
硬件层:磁盘/屏幕硬件缓冲区
而setvbuf控制的是应用层缓冲区。
正常情况下scanf读取数据,是先把我们发送到内核缓冲区的数据放进stdio流缓冲区,再一个字节一个字节的读取,但是这里,我们用setvbuf关闭了应用缓冲层,所以scanf会直接从内核缓冲层一个一个读取,这里的scanf(“%zu”,v2),是读取size_t无符号整形的,所以只会读取65535这几个数字,在读到X的时候,就会停止,并把X回退,但并不是回退到内核层,而是一个专门用来“预读失败回退”的1字节库级回退缓冲区,紧接着read读取,但由于read是系统调用函数,所以它会直接从内核层读取,就可以避开X,直接去读AA,所以这里是一定要放一个字母让scanf停止读取。
同时呢,第二次发送的数据好像就没管scanf了,这是因为scanf会先尝试读取回退缓冲区的数据,结果发现又不匹配,就会直接留在回退缓冲区里,相当于是把scanf卡没了,接着进行后续流程。
结语
这wp,neise1写了很久,希望能给看完的人带来收获。
Ciallo~(∠・ω< )⌒☆







