本文首发于安全客文章 :
Linux pwn系列继续更新。近期终于花了一点时间把自己的坑填上。今天将首先为大家带来上篇文章遗留题目的解答。再次,将介绍两种pwn的方式。这两种pwn都是针对开启了NX保护的程序。其间,还给大家分享了我更新的工具getOverFlowOffset 。 该工具经过升级,能够同时应对开启和没有开启PIE的程序。支持分析32位和64位程序。欢迎大家提issue :)。
“纸上得来终觉浅,绝知此事要躬行” ——《冬夜读书示子聿》
时间久远,怕大家找不到从前的文章,特此给出传送门:Linux pwn从入门到熟练(二) Linux pwn从入门到熟练
前述Linux pwn从入门到熟练(二) 这篇文章留了一道习题pwn7给大家做。下面给出一种参考解答。
可以发现。栈是不可以执行的。但是没有开启PIE/ALSR,即地址随机化。因此IDA查看的函数地址是可以直接使用的。
可以发现,函数gets存在栈溢出漏洞。
这里,我推荐一个自己写的工具getOverFlowOffset 。 该工具经过我的升级,能够同时应对开启和没有开启PIE的程序。 它会自己检测程序是否开启了PIE,对于开启了PIE的程序,它会通过程序里面调用的其他库函数泄露正确的地址,并将存在漏洞的返回地址修正。比如:
在本程序中,没有开启PIE,因此有如下的结果:
可以发现,溢出点距离EBP的距离是108字节。该程序是32位程序,因此距离存储了返回地址的距离是112字节。
从该程序的提示和查看导入函数表我们可以发现,并没有可以直接用于获取shell的系统函数了(如:system, execve)。我们会马上想到上一篇文章 提到的写shellcodes, 构造syscall的方法。但是,我们前面查保护的时候又发现,该程序开启了栈不可执行保护(NX)。因此也是不可能构造shellcode 了。我们需要自己主动的从系统库libc中提取用于获取shell的库函数。
那么我们怎么提取用于获取shell的库函数呢? libc动态库载入时,其内库函数地址的构成:
包括两个主要步骤,
那么如何获取libc的基地址呢? 我们从上述库函数f载入地址的构成就能够窥探出一丝技巧:如果我们泄露任意一个pwn7程序已经载入的属于libc动态库的函数地址f@load(比如__libc_start_main),然后在函数f在libc中的偏移f_offset@libc已知的情况下,就能够反推出libc载入的基地址libc@load了,即:
其中f_offset@libc对于一个确定的动态库libc是固定的,且可以静态的获得。 因此,pwn7漏洞利用的大致步骤为:
这里,为了通过泄露的库函数地址,来获得libc的基地址,我们借助了一个工具: 需要借助的工具。LibcSearch
该工具的安装方法为
一般的使用方法为
为了泄露__libc_start_main地址的栈空间分布变化
上述图中的右侧图展示了对应栈空间里面数值表达的含义。
为了获取shell时栈空间分布变化
注意,选择libc的版本时,选择32位的,即第1个选项。
对于64位程序,有一个可以获取通用ROP的方案,该方案来自于论文: [black asia 2018]return-to-csu: A New Method to Bypass 64-bit Linux ASLRPaper ,Slides
在某些程序中,我们会发现可以用来构造ROP的 gadgets较少。因此可以利用上述通用ROP方案。由于,该方法的核心是利用函数__libc_csu_init中的代码,因此成为ret2csu。 构造ROP的核心步骤包括三点: 其一是获得用于获取shell的库函数地址, 其二是安排该库函数在合适的位置被调用, 其三是如何巧妙的向函数传参数。
主要思想是:在每个64位的linux程序中都有一段初始化的代码,该代码中含有一段可以被用来间接给函数输入参数赋值的代码。
该段通用代码位于__libc_csu_init函数中:
借用论文中的gadgets图来说明调用方式:
在64位的程序中,当参数少于7个时, 参数从左到右放入寄存器: rdi, rsi, rdx, rcx, r8, r9。 因此,对应于上述提到的三点核心的后面两点:其二是安排该库函数在合适的位置被调用 :可以发现,在gadget 2中,可以利用callq来调用地址%r12+%rbx*8指向的函数。我们可以设置rbx=0,那么就变成%r12寄存器指向的函数。而%r12寄存器的值可以利用gadget 1中的代码从栈中指定位置获取。
其三是如何巧妙的向函数传参数 :从gadget 2中可以发现64位程序前三个输入参数存入的寄存器rdi, rsi, rdx分别可以从寄存器r15d, r14, r13中获取值。而结合gadget 1,可以发现r15d, r14, r13的值可以从栈中获取。那么通过合理的分配栈中的数据,我们就可以顺利的控制参数数值了。三个参数对于大部分的漏洞利用而言,基本够用了。
下面以一道zhengmin大神的level 5 , 64位程序来讲解。Pwn8 那么我们回到本题中,迅速的三连。
开启了栈不可执行保护(NX)。没有开启PIE和canary。
溢出的原因是对于char类型变量,可以输入超长的长度。
距离EBP的偏移是128,距离返回地址的覆盖是136字节。
值得注意的是,本题中的__libc_csu_init汇编结果不同,寄存器赋值的顺序也变了。但是只要利用的思路理解了,稍微调整一下即可。
调用write_got泄露write_got地址的栈
序号后面的寄存器内容表示,执行完对应指令后,寄存器的变化。 标记红色的为关键的模块。 包括如何将栈中的地址映射到不同的寄存器中;再到寄存器赋值到64位程序输入参数中;最后到利用callq调用程序;最后修正rsp的指针,来跳转到主函数位置。
调用read_got将字符串/bin/sh加载到bss段中
此处部分的栈布置和前述利用write@got泄露write@got差不多。只是callq调用的函数变成了read@got。输入的参数变成了0, bss_base, 16.表示向地址bss_base输入16个字节。
调用execve执行获得shell
此处的bss_base地址中已经存储了execve的地址。注意,由于callq 调用时,是取目标地址指向的地址来调用函数,因此需要借助bss_base来转储一下内容。即callq [bss_base]=callq execve_address。否则是不会成功的。
运行时注意选择64位的libc库。即第0个选项。
这里解释一下,为什么在放完gadget 2地址之后,要padding 0x38个数据。才能够放入返回地址。
这是因为在执行完callq之后,我们会使得程序往后执行,且不进行跳转。从而可以最终执行到0x400628位置的retn函数,调用到我们布置的main函数,重新开始执行漏洞。我们在csu中设置了rbx=0, rbp=1.从而在执行到0x4005fd的时候,rbx加1,和rbp相等,从而不会执行跳转。继续往后执行,在到达retn之前,0x400624执行了add rsp, 38h的操作,将栈接着抬高了0x38,所以我们需要padding 0x38的数据,才能够让pwn8程序成功获取我们布置的返回地址。
同时,也由上图也可以看出为什么在放置了csu_end_addr之后,不是直接放置rbx参数的地址。因为[rsp+38h_var_30],可以发现该指令取参数是在当前的rsp基础上增加了8的。因此需要padding 8个‘a’。
上述64位的ROP是不是看起来已经很完美了?大家是不是跃跃欲试的想要带着上面这把“屠龙霸刀”到处找64位程序来练练手?恩,怕是要“欲试未半而中道崩殂”了。
看官且瞅瞅我这道菜。pwn9
让我们继续快速三连
仅仅开启了NX。
存在漏洞的是read函数。Buf仅仅申请了0x50个字节长度,然而read允许读取0x60个字节长度。
距离EBP的偏移是80个字节,返回地址是88个字节。
发现 :有没有发现奇怪的点。对!能够允许溢出的长度非常有限,仅仅16个字节,刚好两个寄存器的长度。那么也就仅仅够覆盖EBP和返回地址了。我们看看前面ret2csu的构造,在溢出之后,需要很多字节来部署寄存器rdi, rsi, rdx的值,还要处理调用完函数之后0x38个字节的padding。因此,ret2csu无法直接使用了。我们也可以就此总结,ret2csu虽然通用,但是需要有较大的溢出空间。
怎么办呢?
这里介绍一种fake frame的方式,可以在溢出空间有限的时候,实现ROP。 在介绍这个操作之前,先给大家介绍两个汇编指令:leave和ret。
Leave指令相当于
Ret指令相当于:
一般程序的结束都是leave;retn。如果我们溢出的返回地址同样还是leave;retn,会发生什么呢?我们把两个leave; retn分别转换成上述解释的操作,来一一解释流程。
序号表示,执行完对应指令的操作之后,寄存器的变化情况。 可以发现,在初始栈中原来放置ebp的位置布置成未来要跳转的新的函数块的起始地址,可以将当前的rsp引导过去。而在目标地址的起始位置开始安装如下规律布置内容,就可以连续的调用自己想要的函数,且输入的参数长度可以自定义。 即: fake_frame_i | 要执行的函数地址 | leave ret 地址 | 参数1 | 参数2 | … 其中步骤1~3是原始程序中的leave; ret;后续的4~6是新增加的gadget里面的leave; ret。
基于上述总结的思路,我们就可以构造下面完整的EXP了。
首次的溢出是为了让puts函数输出栈中存储的rsp的值。
为了输出puts@got的地址,栈分布情况
其中0x400793,用于pop第一个输入参数rdi。借助ROPgadget找到:
其中0x400676是用于重新载入有漏洞的read函数的。 其后填充40个字节,是由于前面已经有5*8的位置占用了。 0x4006be是leaver ret的地址。
为了执行execve("/bin/sh",0 ,0)的栈分布情况 :
其中: pop_rdx_pop_rsi_ret=libc_base+0x00000000001306d9 这个部分的地址需要自己借助ROPgadget等工具来找到并且更新,不同机器会不一样。
这里需要解释一下为什么在执行execve的时候,需要stack-48,降低栈的高度来引rsp。
这是因为,在第一次泄露puts@got函数地址,返回到带有漏洞的函数(即0x4000676)继续执行时,存在会改变rsp数值的操作。Rsp改变了,也就导致了溢出数据时的起始地址发生了改变,如果不进行调整,将无法跳转到正确的位置。我们发现在0x4000676有两处操作改变了rsp的数值。
后期跟进栈平衡原则,rsp的内容不会再有变化了。所以,我们这个时候输入payload数据会载入到rsp-48的位置,那么我们代码跳转的位置也需要相应的调整。
执行结果:
最后,照旧给大家留一道练习题来巩固一下。 我们下期见。Pwn10
参考资料:
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)