CTFSHOW卷王杯-pwn
根据官方 wp 学习了两道好题
# check in
# 思路
发现开了 **sandbox** (之后再仔细分析),然后读入姓名那里有一个明显的格式化字符串漏洞,再之后可以读入 0x90  大小的数据,然而数组大小只有 0x80 ,很明显是一个栈溢出,但是溢出的长度非常短,只有 0x10 ,也就是只能覆盖 rbp  和 ret ,在程序的最后有 close(1)  关闭了标准输出的文件描述符,也就是我们无法泄露任何信息,包括最终得到的 flag ,最后再看下此题的保护:没有开 **Canary**  和 **PIE**  保护。
首先,格式化字符串的利用方式很显然,可以用于泄露 **libc** :通过泄露 __libc_start_main + 243 ,即可得到 libc_base 。
再来看栈溢出该如何利用,既然我们只能覆盖到 rbp  和 ret ,其中 ret  是跳转执行的地址,那么就可以考虑何处受 rbp  控制,又方便我们利用,不难想到 read  的时候,是将 0x90  的数据读到栈上的,而栈上的地址就受 rbp  控制,由汇编:
| 1 | 0x4013dd <main+163>: lea rax,[rbp-0x80] | 
可见, read  的第二个参数 rsi (写入数据的地址)就是 rbp-0x80  中的内容,因此,我们可以通过控制 **rbp**  为 **bss**  段上的某地址,然后再通过 **ret**  跳转到 **0x4013dd**  的位置,即可往 **bss**  段上写入内容,再之后通过一个栈迁移,即可跳转到我们读到 bss  段上的 gadget  并执行。
最后,我们来看一下这个 sandbox ,是个黑名单,禁用 socket  那些主要就是为了防止重启输出流造成非预期的,可以先不用管,主要就是发现禁用了 open  的系统调用和 read  相关的系统调用,虽然没有禁 write  相关的系统调用,但是由于有 close(1) ,所以也无法输出,这看似是无法 orw  了,不过仔细分析后可以发现: open  的系统调用虽然被禁用了,但是我们可以用 **openat**  系统调用来代替 **open**  系统调用( libc  中的 open  函数就是对 openat  这个底层系统调用的封装), openat  分绝对路径和相对路径两种写法, exp  中都给出了;再来看 read ,注意到 read  相关的系统调用并非全部被禁用了,当 read  的 fd  为 0  时, read  是可用的,对于常规 orw  来说,先 open  一个文件,由于 0,1,2  都分别被标准输入,输出,报错给占用了,所以文件描述符是从 3  开始的,而若是我们在 open  前,先 **close(0)** ,再 **open**  的话,我们打开的文件的描述符就是 **0**  了,我们也就可以 **read**  读取文件内容了;最后,对于 write  来说,可以采用 **“侧信道攻击” 的方式,就是对 flag  的每一位进行爆破,与我们已经 read  读入到内存中的真实 flag  进行比对,比如,若是相等就触发死循环,那么我们就可以通过判断接收数据用了多久来判断猜测是否正确了,在当前假设下,若是超过了 1  秒,则说明我们这一位爆破猜测成功了,当然,我这里写了一个 “二分法” 的版本,不然会耗费很长时间(其实, CTFshow  的 flag  好像用的是 uuid  字符串,也就是 {}  中的内容仅局限于 -0123456789abcdef  这几个字符,因此,应该还能进一步缩短我 exp  的爆破时长)。由于 “侧信道攻击” 最好使用 shellcode  来实现,故在之前需要用 mprotect  的 gadget  链改一下 bss  段的可执行权限,而一次性只能读入 0x80  大小的数据,可能无法将 orw  的 shellcode  和 mprotect  的 gadget  一起读进 bss  段,因此,我们可以先写一小段 ** **shellcode**  作为跳板和 mprotect  的 gadget  一起读入到 bss  段,再通过这个跳板,将 orw  的 shellcode  读到 bss  段上并跳转执行
# EXP
| 1 | from pwn import * | 
# Incomplete Menu
# 思路
这题给出了一个不完整的菜单,只有 new  和 edit , new  就是新建一个 ** 任意大小(无限制)** 的堆块,最多只可以创建 5  个堆块, edit  可以输入需要读进某堆块中内容的长度 len ,如果输入的长度 len  超过了该堆块的大小 size ,则实际读入长度 Len = size ,否则 Len = len 。漏洞点在于:在将读入内容的最后一字节改为 \x00  的时候,长度用的是用户输入的长度 len ,而并非实际读入的长度 Len ,这样就会导致某堆块后面的任意某字节会被 “刷零”,不过每个堆块只能被 edit  一次。
没有 show ,不能泄露信息,不过有走 IO  流输出的函数,如 puts  和 printf ,因此容易想到通过劫持 stdout  来进行信息泄露,没有 delete  函数,不能对堆块进行 free ,其实可以通过漏洞改 top chunk  的 size ,将它改小以后(要保证后三位不动),再申请一个大堆块,就能将原先的 top chunk  给 free  调了,不过在这里貌似并没有太大的用处。
我们只有这一个可利用的漏洞,又需要劫持到 stdout ,那就需要知道 stdout  与堆块地址的偏移,对于一般的堆块,其地址与 libc  地址的偏移肯定是无法确定的,但是这题可以申请任意大的堆块,也就是可以通过 mmap  申请堆块,而 **mmap**  申请出来的堆块,是紧接在 **libc**  的上方的,其地址与 **libc**  中地址的偏移是可以确定的,这里可以通过将 **_IO_2_1_stdout_**  的 **_IO_read_end**  和 **_IO_write_base**  的最后一字节都改为 **\x00** ,这样他们就相等了,也就可以通过走 IO  的输出函数泄露出其中( _IO_write_base ~ _IO_write_ptr )包含的 libc  地址,进而得到 libc_base 。
泄露出 libc_base  之后,我们肯定是需要一个 “任意写” 漏洞,劫持一些函数或者 IO  流这些才能完成攻击。不难想到,可以通过劫持 stdin  来实现,这里我们按照和上面类似的方式,修改 **_IO_2_1_stdin_**  的 **_IO_buf_base**  中的最后一字节为 **\x00** ,这时, _IO_buf_base  正好指向了 _IO_2_1_stdin_ ,而我们读入的时候,用的是 fgets ,这是一个走 IO  流的读入函数(这个函数就是读一整行到 stdin  缓冲区,然后再从缓冲区取出指定长度的数据,因此读数据会被 \n  截断,或者已经从缓冲区取到了所需长度的数据,也不再会刷新缓冲区往后读取数据了),因此,我们可以通过 fgets  读入任意内容到被伪造的 _IO_buf_base ( _IO_2_1_stdin_ )处,这样就可以再劫持一次 stdin  进行任意写了,我们读入多少字节到缓冲区, _IO_read_end  就会相应加多少,从缓冲区读取多少字节到目标内存, _IO_read_ptr  就会相应加多少,不过,最多也只能一次性读入 _IO_buf_end - _IO_buf_base  大小的数据到缓冲区,如果还需要读入,则会刷新缓冲区,一次也最多只能读取 _IO_read_end - _IO_read_ptr  大小的合法数据到目标内存,此时,由于 _IO_buf_end  为 _IO_buf_base + 132 ,因此,我们只有读满 **132**  个字节,才有机会按我们第一次劫持 **stdin**  后,读入到 **_IO_buf_base**  中的值(记为 **_IO_buf_base(new)** )刷新缓冲区,只有刷新完缓冲区之后,才能按照我们的设想进行第二次 **stdin**  的劫持。这里需要注意的是,在第一次完成 stdin  的劫持,读入 132  字节的内容到 _IO_2_1_stdin_ 中之后,会尝试从缓冲区取 16  个字节到目标内存,如果成功取出了 16  个字节,也就满足了 fgets  的需要,那么也就不会刷新缓冲区了,我们也就不能对 stdin  进行第二次劫持了。在这里, glibc  是通过判断 **_IO_read_ptr**  是否小于 **_IO_read_end**  来判断缓冲区中是否还有剩余的数据,因此,我们可以在第一次劫持 stdin  往 _IO_2_1_stdin_ 中写内容的时候,修改其中的 **_IO_read_ptr**  等于 **_IO_read_end** ,这里的 _IO_read_end  是指读完 132  个字节后的值( _IO_buf_base(new) + 132 ),也就是需要 _IO_read_ptr = _IO_buf_base(new) + 132 ,其实,这里也不一定是要加上 132 ,略小一点,只要保证和 _IO_read_end  差值不足大约 16  个字节,可以有刷新缓冲区的机会即可,并且, glibc  源码中也只是判断了 _IO_read_ptr  是否小于 _IO_read_end ,故还可以将 _IO_read_ptr  改为大于 _IO_read_end ,比如 _IO_read_ptr = _IO_buf_base(new) + 200  也行。在这里,我是通过劫持 **IO_list_all**  来打 **FSOP**  的,通过读取 choice  的 fgets  进行 “任意写” 以后,由于获取到的值并非菜单中的选项 1  或 2 ,就会走到 exit ,直接触发 FSOP 。
# EXP
| 1 | from pwn import * | 
# 参考
1.BUUOJ PWN EXERCISE(二)
2.gyctf_2020_force
3.BUUCTF Pwn Exercise(一)
4.xdctf2015_pwn200
5.堆题总结
6.cmcc_simplerop
7.hitcontraining_unlink
8.0ctf2017-babyheap







