ROP 绕过 NX 技术--pwntools--shellcode 资料 入门基础

最近做pwn频繁地遇到开启了 NX 保护的二进制程序,绕过 NX 保护最常用的方法就是 ROP。网络上关于 ROP 的原理和 CTF 这类题目的文章较多,但是这些文章要不就是给出了一堆代码,要不只是单纯地讲解 CTF 题目和 ROP 原理(写的还不详细),也缺乏系统性地讲解这类 CTF 题目的解题步骤,这通常会阻碍初学者的学习步伐和热情。

函数调用约定

函数的调用约定就是描述参数是怎么传递和由谁平衡堆栈的,以及返回值的。常见的四种调用约定如下:

_stdcall (windowsAPI 默认调用方式)
_cdecl (c/c++默认调用方式)
_fastcall
_thiscall

每种约定的具体内容你可以自行去 Google 学习,但是在 CTF 中你只需要打开 IDA 阅读二进制的汇编代码就可以清晰地看懂程序的参数传参顺序 、栈传参还是寄存器传参 、函数内平衡堆栈还是函数外 。用 IDA直接阅读程序的汇编代码非常关键——第一,出题人很可能会在汇编中做些手脚;第二,国际大佬都是不用 F5 的。

清楚地掌握常用输入输出函数在处理字符串上的区别,尤其是在处理空白字符,\n,\0 上的异同

scanf("%s",str):匹配连续的一串非空白字符,遇到空格、tab 或回车即结束,并在字符串结尾添加 \0,这些空白字符会留在缓冲区;字符串前的空白字符没有存入 str,只表示输入还未开始。
gets(str):可接受回车键之前输入的所有字符,并用'\0'替代 '\n'. 回车键不会留在输入缓冲区中。
printf("%s",str)和puts(str)均是输出到'\0'结束,遇到空格不停,但puts(str)会在结尾输出'\n'
read 和 write 参数虽然较多但是相比前面的函数非常可控,严格按照指定的字节数操作,并且能处理任意字符

什么是 NX 保护

最早的缓冲区溢出攻击,直接在内存栈中写入 shellcode 然后覆盖 EIP 指向这段 shellcode 去执行,所以 NX 即 No-eXecute (不可执行) 的基本原理是将数据所在内存页标识为不可执行,当程序溢出成功转入 shellcode 时,程序会尝试在数据页面上执行指令,此时 CPU 就会抛出异常,而不是去执行恶意指令。

ROP 绕过 NX 原理

这里引用别人的图片和说明。最基本的 ROP 攻击缓冲区溢出漏洞的原理:(图里基于 x64 平台,注意 x64 使用 rdi 寄存器传递第一个函数参数)

在这里插入图片描述

原理:

①当程序运行到 gadget_addr 时(rsp 指向 gadget_addr),接下来会跳转到小片段里执行命令,同时 rsp+8(rsp 指向 bin_sh_addr)
②然后执行 pop rdi, 将 bin_sh_addr 弹入 rdi 寄存器中,同时 rsp + 8(rsp 指向 system_addr)
③执行 return 指令,因为这时 rsp 是指向 system_addr 的,这时就会调用 system 函数,而参数是通过 rdi 传递的,也就是会将 /bin/sh 传入,从而实现调用 system(’/bin/sh’)

所以纵观我们整个 ROP 利用链的环节,有三个很重要的问题需要解决:

怎么去搜索这样的 gadget_addr,当然不止一次 pop,还可以多个 pop 加 ret 组合等等,看你希望怎么去利用
如何得到 '/bin/sh\0' 这样的字符串,通常程序没有这样的字符串
如何得到 libc 中 system 实际运行的地址(libc 的基地址+system 在 libc 中的偏移地址)

其实还有一个问题很重要,就是确定你的返回地址 return_addr 前面缓冲区到底有多大,这样才能准确的实现缓冲区溢出覆盖。做法有二种:一是直接从 IDA 的 F5 源码和汇编计算得到;二是使用 GDB 动态调试一下

ROP 绕过 NX 具体步骤

下面分别就上面三个问题给出具体的解决方案,最终完成整个 ROP 绕过 NX 保护的攻击。

(1)如何搜索你需要的 gadget_addr?

gadget_addr 指向的是程序中可以利用的小片段汇编代码,在上图的示例中使用的是 pop rdi ; ret ;

对于这种搜索,我们可以使用一个工具:ROPgadget

项目地址:https://github.com/JonathanSalwan/ROPgadget.git

(2)bin_sh_addr 指向的是字符串参数:/bin/sh\0。

首先你需要搜索一下程序是否有这样的字符串,但是通常情况下是没有的。这时候就需要我们在程序某处写入这样的字符串供我们利用。我们需要用 IDA 打开程序,看左边函数窗口程序加载了下面哪些函数:

read、scanf、gets

通常我们将这个字符写入 .bss 段。.bss 段是用来保存全局变量的值的,地址固定,并且可以读可写。通过 readelf -S pwnme 这个命令就可以获取到 bss 段的地址了(ida 的 segements 也可以查看)。

(3)system_addr 则指向 libc 中的 system 函数 。

可以先查看一下程序本身有木有可以利用的子函数,这样可以大大减少 EXP 开发时间。因为从 libc 中使用函数,需要知道 libc 的基地址。通常得到 libc 基地址思路就是:

泄露一个 libc 函数的真实地址 => 由于给了 libc.so 文件知道相对偏移地址 => libc 基地址 => 其他任何 libc 函数真实地址

泄露一个 libc 函数的地址需要使用一个能输出的函数,同样用 IDA 打开程序,看左边的函数窗口程序加载了哪些函数可以利用:

write、printf、puts

特别注意:由于 libc 的延迟绑定机制,我们需要选择已经执行过的函数来进行泄露。你需要找到函数的 plt 地址,找到 jmp 指向的那个地址才是我们需要泄露的(参考后文 classic)。

ROP 绕过 NX 实现框架

总结上面的内容,一个基本的的 ROP 绕过 NX 利用的流程是 :

在同一次远程访问中,我们首先通过泄露一些函数真实地址结合相对偏移得到 system 函数的真实地址
执行完上述步骤后,我们需要控制程序再次执行到缓冲区溢出漏洞点
再利用缓冲区溢出漏洞,写入 "/bin/sh\0" 字符串到 .bss,并触发 system 执行

网上很多人会使用 pwntool 的 DynELF 作为泄露的工具,但是这种方法经常会遇到各种问题,尤其是只能利用 puts 函数泄露的时候。另外的做法是自己编写二个 payload 完成上述的利用流程,第一个 payload 去完成泄露并再次到漏洞函数执行,第二个 payload 执行写入 “/bin/sh\0” 字符串到 .bss 并让程序调用 system ,这样得到 shell。

我强烈推荐后面的双 payload做法,也是我去解决后文 classic 题目的做法。因为我最初明白 ROP 步骤中的泄露原理还是通过钻研网上的 DynELF leak 案例搞懂的,下文会先帮助读者分析明白 DynELF leak 函数编写的原理,再讨论我推荐的做法。(当然 pwntool 有 ROP 模块可以直接用,但是这种不是万能,处理一些不常规的题目时效果不好,也不利于学习 ROP 的原理)

刚接触pwn没多久,有些东西理解的还不够,下面负载学习的查找的资料:

pwntools:https://blog.csdn.net/qq_29343201/article/details/51337025http://brieflyx.me/2015/python-module/pwntools-intro/

shellcode: http://blog.nsfocus.net/easy-implement-shellcode-xiangjie/

感觉对入门学习pwn有很好的理解,可以参考下。

  • 1
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值